diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 217f9fb..3fa14fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,4 +39,9 @@ jobs: - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 + env: + # Opt into the Pkg-app shim simulation regression test (#106/#124). + # Requires a resolved Manifest.toml in $ROOT, so only runs in dev + # checkouts — not when JuliaC is exercised as an installed package. + JULIAC_TEST_PKG_APP_SHIM_SIM: "1" diff --git a/Project.toml b/Project.toml index ac3805f..07d339d 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ Patchelf_jll = "f2cf89d6-2bfd-5c44-bd2c-068eea195c0c" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" StructIO = "53d494c1-5632-5724-8f4c-31dff12d585f" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] LazyArtifacts = "1" @@ -24,6 +25,7 @@ Patchelf_jll = "0.18" Pkg = "1" RelocatableFolders = "1" StructIO = "0.3" +TOML = "1" julia = "1.10" [apps.juliac] diff --git a/src/JuliaC.jl b/src/JuliaC.jl index 1520fdf..0a2b2fc 100644 --- a/src/JuliaC.jl +++ b/src/JuliaC.jl @@ -4,6 +4,7 @@ using Pkg using PackageCompiler using LazyArtifacts using RelocatableFolders +using TOML @static if VERSION >= v"1.12.0-rc1" diff --git a/src/compiling.jl b/src/compiling.jl index ee0cba1..9f1b3f0 100644 --- a/src/compiling.jl +++ b/src/compiling.jl @@ -37,6 +37,30 @@ function _start_spinner(message::String; io::IO=stderr) return finished, task end +""" +Inject HostCPUFeatures preferences into a project's LocalPreferences.toml +and ensure the extras section in Project.toml references HostCPUFeatures. + +Merges with any existing preferences/extras rather than overwriting. +""" +function _inject_trim_preferences(project_dir::String) + proj_path = joinpath(project_dir, "Project.toml") + proj = isfile(proj_path) ? TOML.parsefile(proj_path) : Dict{String,Any}() + extras = get!(Dict{String,Any}, proj, "extras") + extras["HostCPUFeatures"] = "3e5b6fbb-0976-4d2c-9146-d79de83f2fb0" + open(proj_path, "w") do io + TOML.print(io, proj) + end + + prefs_path = joinpath(project_dir, "LocalPreferences.toml") + prefs = isfile(prefs_path) ? TOML.parsefile(prefs_path) : Dict{String,Any}() + hcf = get!(Dict{String,Any}, prefs, "HostCPUFeatures") + hcf["freeze_cpu_target"] = true + open(prefs_path, "w") do io + TOML.print(io, prefs) + end +end + function compile_products(recipe::ImageRecipe) # Only strip IR / metadata if not `--trim=no` strip_args = String[] @@ -104,24 +128,16 @@ function compile_products(recipe::ImageRecipe) end project_arg = isdir(project_arg) ? tmp_project : joinpath(tmp_project, basename(project_arg)) - env_overrides = Dict{String,Any}() - tmp_prefs_env = nothing - if is_trim_enabled(recipe) - load_path_sep = Sys.iswindows() ? ";" : ":" - # Create a temporary environment with a LocalPreferences.toml that will be added to JULIA_LOAD_PATH. - tmp_prefs_env = mktempdir() - open(joinpath(tmp_prefs_env, "Project.toml"), "w") do io - println(io, "[extras]") - println(io, "HostCPUFeatures = \"3e5b6fbb-0976-4d2c-9146-d79de83f2fb0\"") - end - # Write LocalPreferences.toml with the trim preferences - open(joinpath(tmp_prefs_env, "LocalPreferences.toml"), "w") do io - println(io, "[HostCPUFeatures]") - println(io, "freeze_cpu_target = true") - end - # Append the temp env to JULIA_LOAD_PATH + # Always clear JULIA_LOAD_PATH to prevent parent environment leakage + # (e.g. when JuliaC is invoked as a Pkg app, the shim sets JULIA_LOAD_PATH + # to JuliaC's own project, which would break user project compilation — #106/#124). + env_overrides = Dict{String,Any}("JULIA_LOAD_PATH" => nothing) - env_overrides["JULIA_LOAD_PATH"] = load_path_sep * tmp_prefs_env + if is_trim_enabled(recipe) + # Inject HostCPUFeatures preferences directly into the temp project copy so + # the preference is visible without JULIA_LOAD_PATH manipulation. + tmp_project_dir = isdir(project_arg) ? project_arg : dirname(project_arg) + _inject_trim_preferences(tmp_project_dir) end inst_cmd = addenv(`$(Base.julia_cmd(cpu_target=precompile_cpu_target)) --project=$project_arg -e "using Pkg; Pkg.instantiate(); Pkg.precompile()"`, env_overrides...) diff --git a/test/DepProject/Project.toml b/test/DepProject/Project.toml new file mode 100644 index 0000000..9992297 --- /dev/null +++ b/test/DepProject/Project.toml @@ -0,0 +1,9 @@ +name = "DepProject" +uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479" +version = "0.1.0" + +[deps] +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[apps] +DepProject = {} diff --git a/test/DepProject/src/DepProject.jl b/test/DepProject/src/DepProject.jl new file mode 100644 index 0000000..55bc6d1 --- /dev/null +++ b/test/DepProject/src/DepProject.jl @@ -0,0 +1,11 @@ +module DepProject + +using SHA + +function @main(ARGS) + h = bytes2hex(sha256("hello")) + println(Core.stdout, "sha256: ", h) + return 0 +end + +end diff --git a/test/cli.jl b/test/cli.jl index ce126c1..6ec54fa 100644 --- a/test/cli.jl +++ b/test/cli.jl @@ -276,3 +276,70 @@ end @test String(take!(io)) == "juliac version $(pkgversion(JuliaC)), julia version $(VERSION)\n" end + +# https://github.com/JuliaLang/JuliaC.jl/issues/106 + #124 +# Simulate the Pkg-app shim env by running `julia -m JuliaC` with +# JULIA_LOAD_PATH=/. This requires $ROOT to contain a +# resolved Manifest.toml, which is only true in a dev checkout — Pkg-installed +# JuliaC has its Manifest.toml gitignored. Gate the test on an opt-in env var +# set in JuliaC's own CI (.github/workflows/ci.yml), so it doesn't run when +# JuliaC is exercised as an installed package (e.g. from Julia's CI). +if get(ENV, "JULIAC_TEST_PKG_APP_SHIM_SIM", "") == "1" +@testset "Pkg app JULIA_LOAD_PATH isolation (#106)" begin + isfile(joinpath(ROOT, "Manifest.toml")) || + error("JULIAC_TEST_PKG_APP_SHIM_SIM=1 requires a resolved \$ROOT/Manifest.toml") + outdir = mktempdir() + exename = "app_pkgapp" + cmd = addenv( + `$(Base.julia_cmd()) --startup-file=no --history-file=no --project=$(ROOT) -m JuliaC + --output-exe $exename $(TEST_PROJ) --bundle $outdir --verbose`, + "JULIA_LOAD_PATH" => ROOT * "/", + ) + @test success(cmd) + actual_exe = Sys.iswindows() ? joinpath(outdir, "bin", exename * ".exe") : joinpath(outdir, "bin", exename) + @test isfile(actual_exe) + if isfile(actual_exe) + output = read(`$actual_exe`, String) + @test occursin("Fast compilation test!", output) + end +end +end + +# End-to-end: install JuliaC as a Pkg app, invoke the shim, compile a project. +# Unix-only: the Pkg app shim is a shell script on Unix, a .cmd on Windows. +if Sys.isunix() +@testset "Pkg app end-to-end (#106)" begin + mktempdir() do depot + outdir = mktempdir() + exename = "app_e2e" + sep = ":" + bindir = joinpath(depot, "bin") + + depot_path = join([depot; Base.DEPOT_PATH], sep) + install_script = """ + using Pkg + Pkg.Apps.develop(; path=$(repr(ROOT))) + """ + install_cmd = addenv( + `$(Base.julia_cmd()) --startup-file=no --history-file=no -e $install_script`, + "JULIA_DEPOT_PATH" => depot_path, + ) + @test success(install_cmd) + + shim = joinpath(bindir, "juliac") + @test isfile(shim) + + build_cmd = addenv( + `$shim --output-exe $exename $(TEST_PROJ) --bundle $outdir --verbose`, + "PATH" => bindir * sep * ENV["PATH"], + ) + @test success(build_cmd) + actual_exe = joinpath(outdir, "bin", exename) + @test isfile(actual_exe) + if isfile(actual_exe) + output = read(`$actual_exe`, String) + @test occursin("Fast compilation test!", output) + end + end +end +end diff --git a/test/programatic.jl b/test/programatic.jl index ad8076d..2b8278f 100644 --- a/test/programatic.jl +++ b/test/programatic.jl @@ -496,6 +496,56 @@ end @test occursin("nthreadpools=1", out) end +# https://github.com/JuliaLang/JuliaC.jl/issues/124 +const DEP_PROJ = abspath(joinpath(@__DIR__, "DepProject")) + +@testset "Project with dependencies (no trim) (#124)" begin + outdir = mktempdir() + exeout = joinpath(outdir, "depproject") + + img = JuliaC.ImageRecipe( + file = DEP_PROJ, + output_type = "--output-exe", + verbose = true, + ) + JuliaC.compile_products(img) + link = JuliaC.LinkRecipe(image_recipe=img, outname=exeout, rpath=JuliaC.RPATH_BUNDLE) + JuliaC.link_products(link) + bun = JuliaC.BundleRecipe(link_recipe=link, output_dir=outdir) + JuliaC.bundle_products(bun) + + actual_exe = Sys.iswindows() ? joinpath(outdir, "bin", basename(exeout) * ".exe") : joinpath(outdir, "bin", basename(exeout)) + @test isfile(actual_exe) + if isfile(actual_exe) + output = read(`$actual_exe`, String) + @test occursin("sha256:", output) + end +end + +@testset "Project with dependencies (trim) (#124)" begin + outdir = mktempdir() + exeout = joinpath(outdir, "depproject_trim") + + img = JuliaC.ImageRecipe( + file = DEP_PROJ, + output_type = "--output-exe", + trim_mode = "safe", + verbose = true, + ) + JuliaC.compile_products(img) + link = JuliaC.LinkRecipe(image_recipe=img, outname=exeout, rpath=JuliaC.RPATH_BUNDLE) + JuliaC.link_products(link) + bun = JuliaC.BundleRecipe(link_recipe=link, output_dir=outdir) + JuliaC.bundle_products(bun) + + actual_exe = Sys.iswindows() ? joinpath(outdir, "bin", basename(exeout) * ".exe") : joinpath(outdir, "bin", basename(exeout)) + @test isfile(actual_exe) + if isfile(actual_exe) + output = read(`$actual_exe`, String) + @test occursin("sha256:", output) + end +end + @testset "Project as File" begin outdir = mktempdir() exeout = joinpath(outdir, "prog_exe_projfile")