From 864ead49c99f6ac474a176e0d8faad5c470efb0d Mon Sep 17 00:00:00 2001 From: Musa Mahmood Date: Sat, 10 Oct 2026 05:06:25 +0100 Subject: [PATCH] [0.1.0] release.jai: a self-contained Windows release, zipped - jai release.jai builds the release build, then stages release/mcap-VERSION-windows-x64/ with mcap.exe and its symbols, onnxruntime (CUDA provider), FFmpeg, the assets, the OCR, label and segmentation models (each checked against its models.config), NVIDIA's CUDA 13 runtime, all of cuDNN 9 and NVRTC, a README and third-party notices; zips it with Windows' tar; prints its SHA-256. - no_cuda leaves the CUDA runtime out; - no_build packages bin/ as it is. - cuDNN is copied whole: it loads sub-libraries by name (9.26's cudnn_engines_tensor_ir64_9.dll), and one missing fails every run on the GPU. - Checked by running the package with no CUDA or cuDNN on PATH: OCR, labels and segmentation on CUDA, video import through its FFmpeg. - release/ is ignored. --- .gitignore | 2 + release.jai | 288 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 release.jai diff --git a/.gitignore b/.gitignore index 11d64cf..12dd019 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ Thumbs.db /modules/Labels/models/ # Segmentation: exported by tools/export_mask2former.py, not committed /modules/Segment/models/ +# Releases: made by release.jai +/release/ diff --git a/release.jai b/release.jai new file mode 100644 index 0000000..89862bb --- /dev/null +++ b/release.jai @@ -0,0 +1,288 @@ +// Makes a release of mcap for Windows x64: one folder with everything it needs to run (the program, +// onnxruntime, FFmpeg, the OCR, label and segmentation models, and NVIDIA's CUDA 13 runtime and +// cuDNN 9, so the GPU works with nothing but the NVIDIA driver installed), zipped: +// +// jai release.jai release/mcap-VERSION-windows-x64/ and release/mcap-VERSION-windows-x64.zip +// jai release.jai - no_cuda without the CUDA runtime (1.3 GB less): the GPU then needs CUDA 13 and cuDNN 9 installed +// jai release.jai - no_build package bin/ as it is, without building it again first +// +// VERSION comes from first.jai. A tree with changes, or a HEAD that is not tagged vVERSION, is warned +// about, not refused. The models must be in bin/ already: OCR's and the labels' from their +// fetch_models.jai, segmentation's from tools/export_mask2former.py (see each module's README). + +#run,stallable { + set_build_options_dc(.{do_output = false}); + args := get_build_options().compile_time_command_line; + with_cuda := !array_find(args, "no_cuda"); + build := !array_find(args, "no_build"); + set_working_directory(#filepath); + + version := read_version(); + if !version fail("Cannot find VERSION in first.jai."); + name := tprint("mcap-%-windows-x64", version); + log("Releasing % (%).", name, ifx with_cuda then "with the CUDA runtime" else "without the CUDA runtime"); + + // Where the source is: warned about, not refused. + status, status_ok := run("git", "status", "--porcelain"); + if status_ok && trim(status) log("Warning: the working tree has changes; the release is built from them."); + tag, tag_ok := run("git", "describe", "--exact-match", "--tags", "HEAD"); + if !tag_ok || trim(tag) != tprint("v%", version) log("Warning: HEAD is not tagged v%.", version); + + if build { + log("Building the release build..."); + _, built := run("jai", "first.jai", "-", "release", print = true); + if !built fail("The build failed."); + } + + // The folder, from nothing. + out := tprint("release/%", name); + if file_exists(out) && (!delete_directory(out) || file_exists(out)) fail(tprint("Cannot delete the last % (is a program or a shell in it?).", out)); + make_directory_if_it_does_not_exist(out, recursive = true); + + // The program and its libraries. + copy("bin/mcap.exe", out); + copy("bin/mcap.pdb", out); // symbols, for crash reports' stack traces + for file_list("bin") { + if ends_with_nocase(it, ".dll") copy(it, out); + } + for string.["data", "fonts", "images"] copy_folder(tprint("bin/%", it), tprint("%/%", out, it)); + + // The models: each folder as the build copied it, checked for the files its models.config names. + for string.["ocr-models", "label-models", "segment-models"] { + check_models(tprint("bin/%", it)); + copy_folder(tprint("bin/%", it), tprint("%/%", out, it)); + } + + // NVIDIA's runtime, beside the program, where Windows looks for a DLL first. + if with_cuda { + missing: [..] string; + for CUDA_FILES { + path := find_cuda_file(it); + if !path { array_add(*missing, it); continue; } + copy(path, out); + } + // All of cuDNN, not a list of it: it loads sub-libraries by name as it needs them (9.26 added + // cudnn_engines_tensor_ir64_9.dll), and one missing fails every run on the GPU. + cudnn := find_cuda_file("cudnn64_9.dll"); + if !cudnn array_add(*missing, "cudnn64_9.dll"); + else for file_list(path_strip_filename(cudnn)) { + name := path_filename(it); + if begins_with_nocase(name, "cudnn") && ends_with_nocase(name, ".dll") copy(it, out); + } + if missing fail(tprint("Cannot find % (on PATH, or under Program Files\\NVIDIA). Install the CUDA 13 toolkit and cuDNN 9 for CUDA 13, or release with `- no_cuda`.", join(..missing, ", "))); + } + + // print has no escape for %, which the README uses: the templates' blanks are filled by name. + readme := replace(RELEASE_README, "{VERSION}", version); + readme = replace(readme, "{GPU}", ifx with_cuda then CUDA_INCLUDED else CUDA_NOT_INCLUDED); + write_entire_file(tprint("%/README.txt", out), readme); + write_entire_file(tprint("%/THIRD-PARTY-NOTICES.txt", out), replace(NOTICES, "{CUDA}", ifx with_cuda then CUDA_NOTICE else "")); + if !copy_file("modules/ffmpeg/windows/LICENSE-BUILD.txt", tprint("%/FFMPEG-BUILD.txt", out)) fail("Cannot copy FFmpeg's build notes."); + + // The zip, with Windows' own tar (bsdtar), which writes zips (and Zip64, past 4 GB). + zip := tprint("release/%.zip", name); + if file_exists(zip) file_delete(zip); + log("Zipping % ...", zip); + _, zipped := run("C:/Windows/System32/tar.exe", "-a", "-c", "-f", zip, "-C", "release", name); + if !zipped fail("Zipping failed."); + + folder_bytes := folder_size(out); + zip_bytes := file_size(zip); + hash_text, hashed := run("certutil", "-hashfile", zip, "SHA256"); + sha := ""; + if hashed { + lines := split(hash_text, "\n"); + if lines.count >= 2 sha = trim(lines[1]); + } + log("Done: % (% MB), from % (% MB).", zip, zip_bytes / (1024 * 1024), out, folder_bytes / (1024 * 1024)); + if sha log("SHA-256 %", sha); +} + +// The CUDA toolkit's files the CUDA provider loads (modules/onnxruntime/cuda.jai: CUDA_LIBRARIES, but +// the driver and cuDNN, which is copied whole), and NVRTC, which cuDNN's runtime-compiled engines load. +CUDA_FILES :: string.[ + "cudart64_13.dll", "cublas64_13.dll", "cublasLt64_13.dll", "cufft64_12.dll", + "nvrtc64_130_0.dll", "nvrtc-builtins64_134.dll", +]; + +// A file of CUDA's or cuDNN's: on PATH (the toolkit's installer puts it there), else the newest cuDNN +// for CUDA 13 under Program Files\NVIDIA\CUDNN (its installer does not). "" when not found. +find_cuda_file :: (name: string) -> string { + for split(path_variable(), ";") { + if !it continue; + path := tprint("%/%", trim_right(it, "\\/"), name); + if file_exists(path) return path; + } + // cuDNN: Program Files/NVIDIA/CUDNN/v9.x/bin/13.y/x64/NAME, the newest (it sorts last). + best := ""; + cudnn_root := "C:/Program Files/NVIDIA/CUDNN"; + if file_exists(cudnn_root) { + for file_list(cudnn_root, recursive = true) { + normal := replace(it, "\\", "/"); + if path_filename(normal) != name || !contains(normal, "/bin/13.") continue; + if compare(normal, best) > 0 best = normal; + } + } + return best; +} + +// Every file a models folder's models.config names in quotes is there. +check_models :: (folder: string) { + config, ok := read_entire_file(tprint("%/models.config", folder), log_errors = false); + if !ok fail(tprint("% has no models.config: build the app first.", folder)); + for raw: split(config, "\n") { + line := trim(raw); + if !line || line[0] == #char "#" || line[0] == #char "[" || line[0] == #char "@" continue; + colon := find_index_from_left(line, #char ":"); + if colon < 0 continue; + key := trim(slice(line, 0, colon)); + value := trim(advance(line, colon + 1)); + if key == "title" || value.count < 2 || value[0] != #char "\"" continue; + file := slice(value, 1, find_index_from_left(advance(value, 1), #char "\"")); + if !file_exists(tprint("%/%", folder, file)) fail(tprint("% names %, which is not there; fetch or make the models (see the module's README).", folder, file)); + } +} + +// This process's PATH. +path_variable :: () -> string { + kernel32 :: #system_library "kernel32"; + GetEnvironmentVariableW :: (name: *u16, buffer: *u16, size: u32) -> u32 #foreign kernel32; + buffer: [32768] u16; // the longest an environment variable can be + count := GetEnvironmentVariableW(utf8_to_wide("PATH",, temp), buffer.data, buffer.count); + if !count || count >= buffer.count return ""; + text, ok := wide_to_utf8(buffer.data, cast(s32) count); + return ifx ok then text else ""; +} + +read_version :: () -> string { + text, ok := read_entire_file("first.jai"); + if !ok return ""; + for raw: split(text, "\n") { + line := trim(raw); + if !begins_with(line, "VERSION ::") continue; + quote := find_index_from_left(line, #char "\""); + if quote < 0 return ""; + rest := advance(line, quote + 1); + return slice(rest, 0, find_index_from_left(rest, #char "\"")); + } + return ""; +} + +// Runs a command; its output, and whether it ran and succeeded. +run :: (args: .. string, print := false) -> output: string, ok: bool { + result, output, error := run_command(..args, capture_and_return_output = !print, print_captured_output = print); + return output, result.type == .EXITED && result.exit_code == 0; +} + +copy :: (source: string, folder: string) { + dest := tprint("%/%", folder, path_filename(source)); + if !copy_file(source, dest) fail(tprint("Cannot copy % to %.", source, dest)); +} + +copy_folder :: (source: string, dest: string) { + if !file_exists(source) fail(tprint("% is not there: build the app first.", source)); + if !copy_directory(source, dest) fail(tprint("Cannot copy % to %.", source, dest)); +} + +file_size :: (path: string) -> s64 { + _, size, ok := file_modtime_and_size(path); + return ifx ok then size else 0; +} + +folder_size :: (folder: string) -> s64 { + total: s64; + for file_list(folder, recursive = true) total += file_size(it); + return total; +} + +fail :: (message: string) { + log_error("Release failed: %", message); + exit(1); +} + +RELEASE_README :: #string END +mcap {VERSION}, for Windows 10 and 11 (x64) + +A library of screenshots, photos and videos, each kept as one .mcap file: imported from any folder +(and Snagit's .snagx), read with OCR, labelled, segmented, and searched. + +Running it + + Unzip this folder anywhere and run mcap.exe. Everything it needs is in this folder. + + The library: mcap shows the .mcap files in D:\McapIndex\mcap. To use another folder, start it with + -library, for example from a shortcut whose target is: + + "C:\Programs\mcap\mcap.exe" -library "E:\My library" + + Import (above the grid) brings in a folder of screenshots, photos, videos or Snagit captures; OCR, + Labels and Segments read them. See README.md in the source for searching and the command line. + +The GPU + +{GPU} + + Without a usable NVIDIA GPU, OCR, labels and segmentation run on the CPU: slower, but the same. + The OCR, Labels and Segments windows say whether the GPU is in use, and if not, why. + +Where it keeps things + + The library's .mcap files hold everything read from them. The app's settings, label list and + segment colours are in %APPDATA%\mcap. + +See THIRD-PARTY-NOTICES.txt for the libraries and models included. +END + +CUDA_INCLUDED :: #string END + NVIDIA's CUDA 13 runtime and cuDNN 9 are included, so OCR, labels and segmentation run on an + NVIDIA GPU (Turing or newer: GeForce RTX 20 series and up) with only the NVIDIA driver installed: + a recent Game Ready or Studio driver that supports CUDA 13. +END + +CUDA_NOT_INCLUDED :: #string END + This release does not include NVIDIA's CUDA runtime. To use an NVIDIA GPU, install the CUDA 13 + toolkit and cuDNN 9 for CUDA 13 from NVIDIA; mcap finds them where their installers put them. +END + +NOTICES :: #string END +Third-party software and models in this release + +onnxruntime 1.30.0 (onnxruntime.dll, onnxruntime_providers_*.dll) + Microsoft, MIT License. https://github.com/microsoft/onnxruntime + +FFmpeg 6.0 (avcodec, avformat, avutil, swscale, swresample DLLs) + The FFmpeg developers. This is gyan.dev's "full" shared build, licensed GPL v3 as a whole: see + FFMPEG-BUILD.txt. Source: https://github.com/FFmpeg/FFmpeg (commit ea3d24bbe3). + +PP-OCRv5 text detection and recognition, and PP-LCNet text line orientation (ocr-models) + PaddlePaddle (Baidu), Apache License 2.0. https://github.com/PaddlePaddle/PaddleOCR + +SigLIP 2 base, patch 16, 384 (label-models) + Google, Apache License 2.0, as converted to ONNX by onnx-community. + https://huggingface.co/google/siglip2-base-patch16-384 + +Mask2Former, Swin-B, COCO panoptic (segment-models) + Meta (Facebook AI Research), from facebook/mask2former-swin-base-coco-panoptic, exported to ONNX by + mcap's tools/export_mask2former.py. https://github.com/facebookresearch/Mask2Former + Its class names are COCO panoptic's (https://cocodataset.org). +{CUDA} +Dear ImGui (in mcap.exe), Omar Cornut, MIT License. stb_image, stb_image_write and +stb_image_resize (in mcap.exe), Sean Barrett, public domain or MIT. +END + +CUDA_NOTICE :: #string END + +NVIDIA CUDA 13 runtime (cudart, cublas, cublasLt, cufft, nvrtc DLLs) and cuDNN 9 (cudnn DLLs) + NVIDIA Corporation, redistributed under the CUDA Toolkit EULA (Attachment A) and the cuDNN + Software License Agreement. https://docs.nvidia.com/cuda/eula/ + https://docs.nvidia.com/deeplearning/cudnn/latest/reference/eula.html +END + +#import "Basic"; +#import "String"; +#import "File"; +#import "File_Utilities"; +#import "Process"; +#import "System"; +#import "Compiler"; +#import "Windows_Utf8";