mcap/release.jai
Musa Mahmood 864ead49c9 [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.
2026-10-10 05:06:25 +01:00

289 lines
12 KiB
Plaintext

// 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";