From 5ff96f8d7623811fc47e6666e1f5f48500146ce5 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 22:48:24 +0000 Subject: [PATCH 01/12] Add precompiled .rsrc resource-directory header asset Ship the 88-byte precompiled .rsrc resource-directory header (src/template/rsrc.bin) used to inject an SxS private-assembly RT_MANIFEST resource into Windows PE products. The blob encodes the resource tree RT_MANIFEST(0x18) -> id 0x2 -> langid 0x409 -> data entry at byte 0x48, with the OffsetToData/Size fields zeroed (patched at injection time). It is pure binary (no embedded strings). sha256: 8e50045a2142f22092d43ada239841c192f79ea9ec1e56697cca2e9e4de001b2 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/template/rsrc.bin | Bin 0 -> 88 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/template/rsrc.bin diff --git a/src/template/rsrc.bin b/src/template/rsrc.bin new file mode 100644 index 0000000000000000000000000000000000000000..995142e3c7042bdff513ad7999deb2a42c582240 GIT binary patch literal 88 rcmZQzfB+US#mFE5q=C2rBFex36K4Vn7+?|S1S;@=DF)I{AQ~6|TwVgN literal 0 HcmV?d00001 From 9765d8fb4d25030b8b03f2cfea6300e550835df2 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 22:49:38 +0000 Subject: [PATCH 02/12] Factor mingw bin/ resolution into shared mingw_bindir/mingw_tool helpers Extract the mingw-w64 artifact bin/ directory lookup out of get_compiler_cmd into reusable mingw_bindir() and mingw_tool(name) helpers so linking and the upcoming Windows privatization share one source of truth for the toolchain location. The Windows branch of get_compiler_cmd now calls mingw_tool, preserving existing behavior. The artifact lookup is wrapped in @static if Sys.iswindows() inside mingw_bindir() because the @artifact_str macro expands at compile time and the artifact entry is os="windows"-gated, so it is unresolvable off-Windows; this keeps the package loadable on non-Windows hosts. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/linking.jl | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/linking.jl b/src/linking.jl index 5038909..92239da 100644 --- a/src/linking.jl +++ b/src/linking.jl @@ -41,6 +41,32 @@ function get_rpath(recipe::LinkRecipe) return string(flag1, " ", flag2) end +# Resolve the bin/ directory of the bundled mingw-w64 toolchain (winlibs MinGW-w64 +# GCC distribution). This artifact is os="windows"-gated and lazy, so this only +# resolves on Windows. It ships the full binutils suite (gcc.exe, g++.exe, objcopy.exe, +# windres.exe, ld.exe, dlltool.exe, as.exe) in one directory. +function mingw_bindir() + # The `@artifact_str` macro expands at compile time and the artifact entry is + # os="windows"-gated, so it is unresolvable off-Windows. Guard with `@static if` + # so the macro is only expanded (and the lookup only attempted) on Windows; on + # other platforms this helper is never reached by the privatization path. + @static if Sys.iswindows() + return joinpath(LazyArtifacts.artifact"mingw-w64", + "extracted_files", + (Int == Int64 ? "mingw64" : "mingw32"), + "bin") + else + error("mingw_bindir() is only available on Windows") + end +end + +# Absolute path to a named tool (e.g. "gcc.exe", "objcopy.exe") in the mingw bin dir. +function mingw_tool(name::AbstractString) + tool = joinpath(mingw_bindir(), name) + isfile(tool) || error("expected mingw tool not found: $tool") + return tool +end + function get_compiler_cmd(; cplusplus::Bool=false) cc = get(ENV, "JULIA_CC", nothing) path = nothing @@ -49,11 +75,7 @@ function get_compiler_cmd(; cplusplus::Bool=false) path = nothing else @static if Sys.iswindows() - path = joinpath(LazyArtifacts.artifact"mingw-w64", - "extracted_files", - (Int==Int64 ? "mingw64" : "mingw32"), - "bin", - cplusplus ? "g++.exe" : "gcc.exe") + path = mingw_tool(cplusplus ? "g++.exe" : "gcc.exe") compiler_cmd = `$path` else compilers_cpp = ("g++", "clang++") From 0beb671e845e3912dfd43b541442c909b797d343 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 22:50:08 +0000 Subject: [PATCH 03/12] Add privatize_windows.jl skeleton: consts and manifest XML generator Create src/privatize_windows.jl with the module header, ObjectFile / StructIO imports, the TEMPLATE_DIR RelocatableFolders constant pointing at the shipped rsrc.bin, the .rsrc data-entry patch offsets (MANIFEST_ADDRESS_OFFSET 0x48, MANIFEST_SIZE_OFFSET 0x4c), and the generate_manifest_xml / manifest_identity_for helpers plus the LIBJULIA_DLL_CANDIDATES tuple. The identity is JuliaC.PrivateRuntime.. The file is not yet included by JuliaC.jl (wired in a later step), so the package still loads unchanged. Manifest generator verified in isolation. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/privatize_windows.jl | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/privatize_windows.jl diff --git a/src/privatize_windows.jl b/src/privatize_windows.jl new file mode 100644 index 0000000..ffc59b4 --- /dev/null +++ b/src/privatize_windows.jl @@ -0,0 +1,67 @@ +""" +Windows-specific privatization for libjulia. + +Unlike the Unix paths (salt-rename via `privatize_libjulia_common!`), Windows has no +SONAME, no symbol versioning, and no DT_NEEDED, so a different mechanism is used: + +1. Inject an SxS private-assembly `RT_MANIFEST` resource into the built product + (`.exe`/`.dll`). Its `` entries list the bundled `libjulia*.dll` siblings, + creating an activation context that makes the loader prefer the copies sitting in the + product's own directory over any same-named DLL already on `PATH`. +2. Strip the stale `../bin/` prefix from the bundled `libjulia.dll`'s embedded, + colon-separated library search path (the flat `bin/` bundle layout has no `../bin/`). + +This file is standalone: it does not use `PrivatizePlatform` / the `plat_*` hooks and never +calls `privatize_libjulia_common!`. +""" + +using ObjectFile +using StructIO +import ObjectFile: COFF, Sections, section_address, section_offset, findfirst + +# The 88-byte precompiled `.rsrc` resource-directory header. It encodes the tree +# RT_MANIFEST(24) -> resource id 2 -> langid 0x409 -> IMAGE_RESOURCE_DATA_ENTRY @ 0x48 +# with the data entry's OffsetToData (0x48) and Size (0x4c) fields zeroed; those are +# patched after objcopy places the section and the loader-relative VA is known. +# Located via RelocatableFolders @path exactly like SCRIPTS_DIR, so it survives app +# bundling / relocation. +const TEMPLATE_DIR = @path joinpath(@__DIR__, "template") + +# Offsets of the two patched UInt32 fields, measured from the start of the .rsrc section. +# Derived by decoding rsrc.bin against the PE format spec: +# https://learn.microsoft.com/windows/win32/debug/pe-format#the-rsrc-section +# (0x00..0x47 = directory tree, 0x48 = IMAGE_RESOURCE_DATA_ENTRY). +const MANIFEST_ADDRESS_OFFSET = UInt(0x48) # IMAGE_RESOURCE_DATA_ENTRY.OffsetToData +const MANIFEST_SIZE_OFFSET = UInt(0x4c) # IMAGE_RESOURCE_DATA_ENTRY.Size + +""" + generate_manifest_xml(identity_name, dll_names) -> Vector{UInt8} + +Build the SxS private-assembly RT_MANIFEST XML. `identity_name` is the assembly identity +label; `dll_names` is the list of DLL filenames to redirect to the product's own directory. +Returns the UTF-8 bytes. +""" +function generate_manifest_xml(identity_name::AbstractString, dll_names) + io = IOBuffer() + print(io, "\n") + print(io, " \n") + for dll in dll_names + print(io, " \n") + end + print(io, "\n") + return Vector{UInt8}(String(take!(io))) +end + +# Build a per-product SxS assembly identity from the product basename (avoids identity +# clashes in the SxS cache when multiple bundles co-reside). Sanitized to the name-safe +# charset; generic prefix (no domain language). +function manifest_identity_for(product_path::AbstractString) + stem = first(splitext(basename(product_path))) # strip .exe/.dll + safe = replace(stem, r"[^A-Za-z0-9._-]" => "_") + return string("JuliaC.PrivateRuntime.", safe) +end + +# The libjulia DLLs the manifest may redirect, in canonical order. +const LIBJULIA_DLL_CANDIDATES = + ("libjulia.dll", "libjulia-internal.dll", "libjulia-codegen.dll") From 2082656fea1fac6c9c52f268e40fb56d23f529e8 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 22:51:32 +0000 Subject: [PATCH 04/12] Add inject_private_manifest!: .rsrc build + objcopy + COFF patch Append inject_private_manifest!(product_path, dll_names) to privatize_windows.jl. It builds the .rsrc section payload (precompiled header ++ generated manifest ++ 4-byte pad), adds it as a .rsrc section via the mingw objcopy tool, then patches the COFF optional header's ResourceTable data directory (VirtualAddress/Size) and the section's internal IMAGE_RESOURCE_DATA_ENTRY OffsetToData/Size fields. The COFF offset math, COFFImageDataDirectory pack, and ResourceTable field offset were verified on Linux against real 32-bit and 64-bit PE binaries (computed datadirs offset reproduces the actual .rsrc section VA). Running objcopy.exe and resolving the mingw artifact are Windows-CI-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/privatize_windows.jl | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/privatize_windows.jl b/src/privatize_windows.jl index ffc59b4..8105628 100644 --- a/src/privatize_windows.jl +++ b/src/privatize_windows.jl @@ -65,3 +65,63 @@ end # The libjulia DLLs the manifest may redirect, in canonical order. const LIBJULIA_DLL_CANDIDATES = ("libjulia.dll", "libjulia-internal.dll", "libjulia-codegen.dll") + +""" + inject_private_manifest!(product_path, dll_names) + +Add the SxS `RT_MANIFEST` resource (listing `dll_names`) to the PE at `product_path`: +build a `.rsrc` payload (precompiled header ++ generated manifest ++ 4-byte pad), add it as +a `.rsrc` section with mingw `objcopy`, then patch the COFF optional header's ResourceTable +data directory and the section's internal manifest address/size fields. +""" +function inject_private_manifest!(product_path, dll_names) + header = read(joinpath(TEMPLATE_DIR, "rsrc.bin")) + manifest = generate_manifest_xml(manifest_identity_for(product_path), dll_names) + + # Build the section payload: header ++ manifest ++ pad-to-4-bytes. + sectionfile = joinpath(dirname(product_path), "rsrc.bin") + open(sectionfile, "w") do rsrc_bin + write(rsrc_bin, header) + write(rsrc_bin, manifest) + if length(manifest) % sizeof(UInt32) != 0 + padding_size = sizeof(UInt32) - length(manifest) % sizeof(UInt32) + write(rsrc_bin, zeros(UInt8, padding_size)) + end + end + + # Add the section using objcopy from the mingw artifact already used for linking. + objcopy = mingw_tool("objcopy.exe") # WINDOWS-CI-ONLY: artifact + run + run(`$objcopy --add-section .rsrc=$sectionfile --set-section-flags .rsrc=data $product_path`) + rm(sectionfile) + + # Re-open and patch the headers now that objcopy has placed the section. + open(product_path, read=true, write=true, create=false, truncate=false) do io + oh = only(ObjectFile.readmeta(io)) + rsrc_section = findfirst(Sections(oh), ".rsrc") + rsrc_section === nothing && error("objcopy did not create a .rsrc section in $product_path") + + # 1) Patch the optional header's ResourceTable data directory. + magic = oh.opt_header.standard.Magic + datadirs_offset = if magic == COFF.OPTHEADER_STANDARD_MAGIC32 + oh.header_offset + sizeof(COFF.COFFHeader) + sizeof(COFF.COFFOptionalHeaderStandard) + + sizeof(UInt32) + sizeof(COFF.COFFOptionalHeaderWindows32) + elseif magic == COFF.OPTHEADER_STANDARD_MAGIC64 + oh.header_offset + sizeof(COFF.COFFHeader) + sizeof(COFF.COFFOptionalHeaderStandard) + + sizeof(COFF.COFFOptionalHeaderWindows64) + else + error("unexpected COFF optional-header magic: 0x$(string(magic, base=16))") + end + seek(oh, datadirs_offset + PatchVersion.fieldname_offset(COFF.COFFDataDirectories, :ResourceTable)) + pack(ObjectFile.handle(oh).io, COFF.COFFImageDataDirectory( + #= VirtualAddress =# section_address(rsrc_section), + #= Size =# rsrc_section.section.VirtualSize, + )) + + # 2) Patch the .rsrc data-entry's manifest address + size (relative until VA known). + seek(oh, section_offset(rsrc_section) + MANIFEST_ADDRESS_OFFSET) + write(ObjectFile.handle(oh).io, UInt32(section_address(rsrc_section) + sizeof(header))) + seek(oh, section_offset(rsrc_section) + MANIFEST_SIZE_OFFSET) + write(ObjectFile.handle(oh).io, UInt32(length(manifest))) + end + return nothing +end From 794df820a4098dbeb632788c1d1cfba4164b1aa1 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 22:53:03 +0000 Subject: [PATCH 05/12] Add fix_libjulia_libpath!, present_libjulia_dlls, and entry point Append the in-place libpath fix that strips the stale ../bin/ prefix (and @../bin/ -> @) from the bundled libjulia.dll's embedded, colon-separated, NUL-terminated library search path; present_libjulia_dlls to list the libjulia*.dll files actually present in the bundle bin/ dir (so trim builds that drop libjulia-codegen.dll produce a correct manifest); and the standalone privatize_libjulia_windows! entry point that injects the manifest into the product and fixes the libpath, with the try/catch + error(..., e) convention matching the macOS/Linux wrappers. The libpath rewrite algorithm was verified on Linux fixtures: rewrites ../bin/a:../bin/b:@../bin/c -> a:b:@c, leaves prefix-less entries untouched (../bin/x:plainentry:@../bin/y -> x:plainentry:@y), preserves trailing bytes, and errors with "Unable to open" on a missing file. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/privatize_windows.jl | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/privatize_windows.jl b/src/privatize_windows.jl index 8105628..6ac74c5 100644 --- a/src/privatize_windows.jl +++ b/src/privatize_windows.jl @@ -125,3 +125,63 @@ function inject_private_manifest!(product_path, dll_names) end return nothing end + +""" + fix_libjulia_libpath!(libjulia_path) + +Strip the stale `../bin/` prefix from the bundled `libjulia.dll`'s embedded, colon-separated +library search path (rewriting `@../bin/` -> `@`), in place, NUL-terminated. The rewrite only +removes bytes, so it never grows the string and is safe to do in place. +""" +function fix_libjulia_libpath!(libjulia_path) + if !isfile(libjulia_path) + error("Unable to open libjulia.dll at $(libjulia_path)") + end + open(libjulia_path, read = true, write = true) do io + needle = "../bin/" + readuntil(io, needle) + skip(io, -length(needle)) + libpath_offset = position(io) + + libpath = split(String(readuntil(io, UInt8(0))), ":") + libpath = map(libpath) do l + if startswith(l, "../bin/") + return l[8:end] + elseif startswith(l, "@../bin/") + return "@" * l[9:end] + end + return l + end + + seek(io, libpath_offset) + write(io, join(libpath, ":")) + write(io, UInt8(0)) + end +end + +# The libjulia DLLs actually present in the bundle bin/ dir, in canonical order. +function present_libjulia_dlls(bindir) + return [dll for dll in LIBJULIA_DLL_CANDIDATES if isfile(joinpath(bindir, dll))] +end + +""" +Windows privatization entry point: inject the SxS manifest into the built product and fix +the bundled libjulia.dll's embedded libpath. Standalone; does not use the plat_* hooks. +""" +function privatize_libjulia_windows!(recipe::BundleRecipe) + try + # On Windows the bundle libdir is "bin" and the product + DLLs are co-located there. + bindir = joinpath(recipe.output_dir, recipe.libdir) + product = recipe.link_recipe.outname + libjulia = joinpath(bindir, "libjulia.dll") + + dll_names = present_libjulia_dlls(bindir) + isempty(dll_names) && error("no libjulia*.dll found in $bindir to privatize") + + inject_private_manifest!(product, dll_names) + fix_libjulia_libpath!(libjulia) + catch e + error("Failed to privatize libjulia on Windows", e) + end + return nothing +end From 54817db0032ed9f5019acc4cef6b63b5bd4e4539 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 22:53:38 +0000 Subject: [PATCH 06/12] Wire privatize_windows.jl include and Windows privatization dispatch Add include("privatize_windows.jl") after privatize_macos.jl in JuliaC.jl, and replace the Windows @warn no-op in privatize_libjulia! with an elseif Sys.iswindows() branch dispatching to privatize_libjulia_windows!. The full package loads with all new symbols defined, the patch-offset consts correct (0x48/0x4c), and the TEMPLATE_DIR RelocatableFolders path resolving to the 88-byte rsrc.bin. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/JuliaC.jl | 1 + src/bundling.jl | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/JuliaC.jl b/src/JuliaC.jl index 1520fdf..16ceb08 100644 --- a/src/JuliaC.jl +++ b/src/JuliaC.jl @@ -67,6 +67,7 @@ include("patchversion.jl") include("privatize_common.jl") include("privatize_linux.jl") include("privatize_macos.jl") +include("privatize_windows.jl") export ImageRecipe, LinkRecipe, BundleRecipe export compile_products, link_products, bundle_products diff --git a/src/bundling.jl b/src/bundling.jl index 66d74b1..e8ba8a9 100644 --- a/src/bundling.jl +++ b/src/bundling.jl @@ -111,6 +111,8 @@ function privatize_libjulia!(recipe::BundleRecipe) privatize_libjulia_macos!(recipe) elseif Sys.islinux() privatize_libjulia_linux!(recipe) + elseif Sys.iswindows() + privatize_libjulia_windows!(recipe) else @warn "Privatization not implemented for this OS" end From f81cbcac14f134b00a93e2f54a73487364157048 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 22:59:45 +0000 Subject: [PATCH 07/12] Add guarded Windows privatization testset Add a Sys.iswindows()-guarded testset inside the Library flows (trim) block that builds a privatized library bundle, then asserts the injected .rsrc resource directory is present (read via ObjectFile), the COFF optional-header ResourceTable data directory points at the .rsrc section VA (read independently via raw seek/read, recomputing the datadirs offset from the file), and the bundled libjulia.dll no longer contains the ../bin/ prefix. No objdump/dumpbin dependency. ObjectFile is referenced as JuliaC.ObjectFile since it is a JuliaC dep but not in the test target extras. On Linux the body is skipped; the full suite passes (Pkg.test). The Windows assertions run on windows-latest CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/programatic.jl | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/programatic.jl b/test/programatic.jl index ad8076d..bfde627 100644 --- a/test/programatic.jl +++ b/test/programatic.jl @@ -261,6 +261,52 @@ @test ver.minor == 32767 end end + + @testset "Windows privatization injects manifest + fixes libpath" begin + if Sys.iswindows() + outdir = mktempdir() + libname = "libwinprivtest" + libout = joinpath(outdir, libname) + link = JuliaC.LinkRecipe(image_recipe=img_lib, outname=libout, rpath=JuliaC.RPATH_BUNDLE) + JuliaC.link_products(link) + bun = JuliaC.BundleRecipe(link_recipe=link, output_dir=outdir, privatize=true) + JuliaC.bundle_products(bun) + + product = joinpath(outdir, "bin", libname * "." * Base.BinaryPlatforms.platform_dlext()) + @test isfile(product) + + # (a) The product PE now has a .rsrc section (read via ObjectFile, already a dep). + rsrc_va = open(product) do io + oh = only(JuliaC.ObjectFile.readmeta(io)) + rsrc = JuliaC.ObjectFile.findfirst(JuliaC.ObjectFile.Sections(oh), ".rsrc") + @test rsrc !== nothing + rsrc === nothing ? UInt32(0) : UInt32(JuliaC.ObjectFile.section_address(rsrc)) + end + + # (b) The optional header's ResourceTable data directory now points at .rsrc. + # Read it with raw seek/read to mirror the ld_flags testset's style and to + # independently confirm the COFF patch landed (datadirs_offset reasoning + # verified against a real PE during planning). + rt = open(product) do io + seek(io, 0x3C); pe_off = read(io, UInt32) + opt_hdr = pe_off + 4 + 20 # PE sig (4) + COFF header (20) + seek(io, opt_hdr); magic = read(io, UInt16) + # 64-bit: standard(24) + windows64(88) = 112 to the data directories; + # 32-bit: standard(24) + BaseOfData(4) + windows32(68) = 96. + dd = opt_hdr + (magic == 0x20b ? 112 : 96) + seek(io, dd + 0x10) # ResourceTable slot + (va = read(io, UInt32), size = read(io, UInt32)) + end + @test rt.va == rsrc_va + @test rt.va != 0 + @test rt.size != 0 + + # (c) The bundled libjulia.dll no longer contains the "../bin/" prefix. + libjulia = joinpath(outdir, "bin", "libjulia.dll") + @test isfile(libjulia) + @test !occursin("../bin/", String(read(libjulia))) + end + end end @testset "Programmatic binary (trim)" begin From f63db7f81be5e456243704b3a935176b88f516a2 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 23:39:07 +0000 Subject: [PATCH 08/12] Condense multi-line comments to single lines Comment-only: trim the three verbose explanatory blocks in the Windows privatization file down to one line each. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/privatize_windows.jl | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/privatize_windows.jl b/src/privatize_windows.jl index 6ac74c5..e4885e2 100644 --- a/src/privatize_windows.jl +++ b/src/privatize_windows.jl @@ -19,18 +19,10 @@ using ObjectFile using StructIO import ObjectFile: COFF, Sections, section_address, section_offset, findfirst -# The 88-byte precompiled `.rsrc` resource-directory header. It encodes the tree -# RT_MANIFEST(24) -> resource id 2 -> langid 0x409 -> IMAGE_RESOURCE_DATA_ENTRY @ 0x48 -# with the data entry's OffsetToData (0x48) and Size (0x4c) fields zeroed; those are -# patched after objcopy places the section and the loader-relative VA is known. -# Located via RelocatableFolders @path exactly like SCRIPTS_DIR, so it survives app -# bundling / relocation. +# Dir holding rsrc.bin (88-byte precompiled RT_MANIFEST header); @path so it survives bundling. const TEMPLATE_DIR = @path joinpath(@__DIR__, "template") -# Offsets of the two patched UInt32 fields, measured from the start of the .rsrc section. -# Derived by decoding rsrc.bin against the PE format spec: -# https://learn.microsoft.com/windows/win32/debug/pe-format#the-rsrc-section -# (0x00..0x47 = directory tree, 0x48 = IMAGE_RESOURCE_DATA_ENTRY). +# Offsets of the two patched UInt32 fields from the start of the .rsrc section (IMAGE_RESOURCE_DATA_ENTRY @ 0x48). const MANIFEST_ADDRESS_OFFSET = UInt(0x48) # IMAGE_RESOURCE_DATA_ENTRY.OffsetToData const MANIFEST_SIZE_OFFSET = UInt(0x4c) # IMAGE_RESOURCE_DATA_ENTRY.Size @@ -53,9 +45,7 @@ function generate_manifest_xml(identity_name::AbstractString, dll_names) return Vector{UInt8}(String(take!(io))) end -# Build a per-product SxS assembly identity from the product basename (avoids identity -# clashes in the SxS cache when multiple bundles co-reside). Sanitized to the name-safe -# charset; generic prefix (no domain language). +# Per-product SxS assembly identity from the product basename (avoids SxS-cache clashes), name-safe. function manifest_identity_for(product_path::AbstractString) stem = first(splitext(basename(product_path))) # strip .exe/.dll safe = replace(stem, r"[^A-Za-z0-9._-]" => "_") From 4cece4366a24be140147614e5bb49041b2540ecd Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 28 May 2026 23:45:19 +0000 Subject: [PATCH 09/12] Use open(file; kwargs...) keyword-separator style Co-Authored-By: Claude Opus 4.8 (1M context) --- src/privatize_windows.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/privatize_windows.jl b/src/privatize_windows.jl index e4885e2..9e9f25f 100644 --- a/src/privatize_windows.jl +++ b/src/privatize_windows.jl @@ -85,7 +85,7 @@ function inject_private_manifest!(product_path, dll_names) rm(sectionfile) # Re-open and patch the headers now that objcopy has placed the section. - open(product_path, read=true, write=true, create=false, truncate=false) do io + open(product_path; read=true, write=true, create=false, truncate=false) do io oh = only(ObjectFile.readmeta(io)) rsrc_section = findfirst(Sections(oh), ".rsrc") rsrc_section === nothing && error("objcopy did not create a .rsrc section in $product_path") @@ -127,7 +127,7 @@ function fix_libjulia_libpath!(libjulia_path) if !isfile(libjulia_path) error("Unable to open libjulia.dll at $(libjulia_path)") end - open(libjulia_path, read = true, write = true) do io + open(libjulia_path; read = true, write = true) do io needle = "../bin/" readuntil(io, needle) skip(io, -length(needle)) From 5954e0477d93b80814b824c45c7daaa1575c94dc Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Fri, 29 May 2026 23:10:25 +0000 Subject: [PATCH 10/12] test: verify privatized product loads its own runtime copy on Windows Add a Windows-gated test that loads the bundled, privatized product within a fresh Julia process and confirms `Libdl.dllist()` reports two distinct copies of the runtime libraries: the host process's already-loaded copy plus the bundled private copy that the side-by-side (SxS) manifest steers the product to load from its own directory. Also checks that the exported symbol still resolves through the privatized runtime. Gated to `Sys.iswindows()` so the suite is a no-op elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/programatic.jl | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/programatic.jl b/test/programatic.jl index bfde627..dad4a4a 100644 --- a/test/programatic.jl +++ b/test/programatic.jl @@ -307,6 +307,68 @@ @test !occursin("../bin/", String(read(libjulia))) end end + + @testset "Privatized library loads its own runtime copy (Windows)" begin + # Behavioral guarantee of the SxS-manifest privatization: when the bundled, + # privatized product is loaded into a Julia process that already has the + # runtime loaded, the activation context steers the product to its own + # sibling runtime DLLs rather than the ones already on PATH. The result is + # two DISTINCT copies of the runtime libraries visible in `Libdl.dllist()`: + # the host process's already-loaded copy plus the bundled private copy. + if Sys.iswindows() + outdir = mktempdir() + libname = "libwinprivloadtest" + libout = joinpath(outdir, libname) + link = JuliaC.LinkRecipe(image_recipe=img_lib, outname=libout, rpath=JuliaC.RPATH_BUNDLE) + JuliaC.link_products(link) + bun = JuliaC.BundleRecipe(link_recipe=link, output_dir=outdir, privatize=true) + JuliaC.bundle_products(bun) + + bindir = joinpath(outdir, "bin") + product = joinpath(bindir, libname * "." * Base.BinaryPlatforms.platform_dlext()) + @test isfile(product) + @test isfile(joinpath(bindir, "libjulia.dll")) + + # Load the privatized product from a fresh Julia process (which already + # has the runtime loaded), then inspect `Libdl.dllist()`. We run it in a + # subprocess so the host test process is unaffected, mirroring the Unix + # "Julia dlopen test" above. + product_literal = repr(product) + bindir_literal = repr(bindir) + snippet = """ + using Libdl + # Capture the runtime libraries already loaded by this host process. + before = filter(p -> occursin("libjulia", lowercase(basename(p))), Libdl.dllist()) + h = Libdl.dlopen($product_literal, Libdl.RTLD_LOCAL) + try + # Confirm the exported function works once loaded. + r = ccall(Libdl.dlsym(h, :jc_add_one), Cint, (Cint,), 41) + println("RESULT=", r) + after = filter(p -> occursin("libjulia", lowercase(basename(p))), Libdl.dllist()) + # Distinct directories holding a libjulia*.dll: expect the host's + # copy plus the bundled private copy (>= 2 distinct directories). + dirs = unique(map(p -> lowercase(abspath(dirname(p))), after)) + bundled = lowercase(abspath($bindir_literal)) + println("HOST_LIBJULIA_COUNT=", length(before)) + println("DISTINCT_DIRS=", length(dirs)) + println("HAS_BUNDLED=", bundled in dirs) + finally + try Libdl.dlclose(h) catch end + end + """ + out = read(`$(Base.julia_cmd()) --startup-file=no --history-file=no -e $snippet`, String) + + # The exported symbol still resolves through the privatized runtime. + @test occursin("RESULT=42", out) + # The bundled directory is one of the distinct runtime-library locations. + @test occursin("HAS_BUNDLED=true", out) + # At least two distinct directories now host a copy of the runtime libs: + # the host/system one and the bundled private one. + m = match(r"DISTINCT_DIRS=(\d+)", out) + @test m !== nothing + @test parse(Int, m.captures[1]) >= 2 + end + end end @testset "Programmatic binary (trim)" begin From 909b33704e8c1d22409051c0038850639187bf0c Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sat, 30 May 2026 00:17:18 +0000 Subject: [PATCH 11/12] Condense verbose comments in the Windows dllist test Collapse the wrapped explanatory comment blocks in the privatized-load testset to single lines. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/programatic.jl | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/test/programatic.jl b/test/programatic.jl index dad4a4a..6ab41ad 100644 --- a/test/programatic.jl +++ b/test/programatic.jl @@ -309,12 +309,7 @@ end @testset "Privatized library loads its own runtime copy (Windows)" begin - # Behavioral guarantee of the SxS-manifest privatization: when the bundled, - # privatized product is loaded into a Julia process that already has the - # runtime loaded, the activation context steers the product to its own - # sibling runtime DLLs rather than the ones already on PATH. The result is - # two DISTINCT copies of the runtime libraries visible in `Libdl.dllist()`: - # the host process's already-loaded copy plus the bundled private copy. + # SxS-manifest guarantee: loaded into a process that already has the runtime, the product loads its own sibling DLLs, so Libdl.dllist() shows two distinct runtime copies. if Sys.iswindows() outdir = mktempdir() libname = "libwinprivloadtest" @@ -329,10 +324,7 @@ @test isfile(product) @test isfile(joinpath(bindir, "libjulia.dll")) - # Load the privatized product from a fresh Julia process (which already - # has the runtime loaded), then inspect `Libdl.dllist()`. We run it in a - # subprocess so the host test process is unaffected, mirroring the Unix - # "Julia dlopen test" above. + # Load the product in a fresh Julia process (which already has the runtime loaded) and inspect Libdl.dllist(), mirroring the Unix "Julia dlopen test" above. product_literal = repr(product) bindir_literal = repr(bindir) snippet = """ @@ -345,8 +337,7 @@ r = ccall(Libdl.dlsym(h, :jc_add_one), Cint, (Cint,), 41) println("RESULT=", r) after = filter(p -> occursin("libjulia", lowercase(basename(p))), Libdl.dllist()) - # Distinct directories holding a libjulia*.dll: expect the host's - # copy plus the bundled private copy (>= 2 distinct directories). + # Distinct dirs holding a libjulia*.dll: host copy + bundled copy (>= 2). dirs = unique(map(p -> lowercase(abspath(dirname(p))), after)) bundled = lowercase(abspath($bindir_literal)) println("HOST_LIBJULIA_COUNT=", length(before)) @@ -362,8 +353,7 @@ @test occursin("RESULT=42", out) # The bundled directory is one of the distinct runtime-library locations. @test occursin("HAS_BUNDLED=true", out) - # At least two distinct directories now host a copy of the runtime libs: - # the host/system one and the bundled private one. + # >= 2 distinct dirs now host the runtime libs: host/system + bundled. m = match(r"DISTINCT_DIRS=(\d+)", out) @test m !== nothing @test parse(Int, m.captures[1]) >= 2 From 388e64f0e784d9cf77771af28cff2fdd962ea7cd Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sat, 30 May 2026 13:42:54 +0000 Subject: [PATCH 12/12] Instrument Windows load test: localize the hang + add a watchdog Add flushed progress markers (M1_PRE_DLOPEN .. M5_POST_CCALL) so the CI log shows whether the hang is at SxS activation (load) or the first-ccall runtime init, and report Libdl.dllist() dirs right after dlopen (before the ccall) to tell whether the product bound its own private libjulia. Run the subprocess under a 180s watchdog so the job can never again consume the 6h CI limit. Diagnostic step toward fixing the Windows-only deadlock; not the final form. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/programatic.jl | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/test/programatic.jl b/test/programatic.jl index 6ab41ad..fe3e67c 100644 --- a/test/programatic.jl +++ b/test/programatic.jl @@ -325,29 +325,40 @@ @test isfile(joinpath(bindir, "libjulia.dll")) # Load the product in a fresh Julia process (which already has the runtime loaded) and inspect Libdl.dllist(), mirroring the Unix "Julia dlopen test" above. + # Flushed markers localize the hang (M2 dirs right after dlopen tells us whether + # SxS activated, BEFORE the first ccall that triggers the product's runtime init); + # the watchdog ensures CI can never again hit the 6h job limit. product_literal = repr(product) bindir_literal = repr(bindir) snippet = """ using Libdl - # Capture the runtime libraries already loaded by this host process. - before = filter(p -> occursin("libjulia", lowercase(basename(p))), Libdl.dllist()) + errln(s) = (println(stderr, s); flush(stderr)) + libdirs() = unique(map(p -> lowercase(abspath(dirname(p))), + filter(p -> occursin("libjulia", lowercase(basename(p))), Libdl.dllist()))) + errln("M1_PRE_DLOPEN dirs=" * string(libdirs())) h = Libdl.dlopen($product_literal, Libdl.RTLD_LOCAL) - try - # Confirm the exported function works once loaded. - r = ccall(Libdl.dlsym(h, :jc_add_one), Cint, (Cint,), 41) - println("RESULT=", r) - after = filter(p -> occursin("libjulia", lowercase(basename(p))), Libdl.dllist()) - # Distinct dirs holding a libjulia*.dll: host copy + bundled copy (>= 2). - dirs = unique(map(p -> lowercase(abspath(dirname(p))), after)) - bundled = lowercase(abspath($bindir_literal)) - println("HOST_LIBJULIA_COUNT=", length(before)) - println("DISTINCT_DIRS=", length(dirs)) - println("HAS_BUNDLED=", bundled in dirs) - finally - try Libdl.dlclose(h) catch end - end + errln("M2_POST_DLOPEN dirs=" * string(libdirs())) # bundled dir present here => SxS activated + sym = Libdl.dlsym(h, :jc_add_one) + errln("M3_POST_DLSYM") + errln("M4_PRE_CCALL") + r = ccall(sym, Cint, (Cint,), 41) # first ccall triggers the product's runtime init + errln("M5_POST_CCALL r=" * string(r)) + println("RESULT=", r) + d = libdirs() + println("DISTINCT_DIRS=", length(d)) + println("HAS_BUNDLED=", lowercase(abspath($bindir_literal)) in d) + try Libdl.dlclose(h) catch end """ - out = read(`$(Base.julia_cmd()) --startup-file=no --history-file=no -e $snippet`, String) + logf = joinpath(outdir, "winload.log") + proc = run(pipeline(`$(Base.julia_cmd()) --startup-file=no --history-file=no -e $snippet`; + stdout=logf, stderr=logf); wait=false) + @async begin + sleep(180) + Base.process_running(proc) && (kill(proc); @warn "Windows load test exceeded 180s; killed") + end + wait(proc) + out = isfile(logf) ? read(logf, String) : "" + @info "Windows privatized-load diagnostic:\n$out" # The exported symbol still resolves through the privatized runtime. @test occursin("RESULT=42", out)