diff --git a/.gitignore b/.gitignore index be81923..f2b7de1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .DS_Store -*Manifest.toml +*Manifest*.toml .vscode \ No newline at end of file diff --git a/Project.toml b/Project.toml index 3ff423a..f060283 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" Patchelf_jll = "f2cf89d6-2bfd-5c44-bd2c-068eea195c0c" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" StructIO = "53d494c1-5632-5724-8f4c-31dff12d585f" [compat] @@ -23,6 +24,7 @@ PackageCompiler = "2" Patchelf_jll = "0.18" Pkg = "1" RelocatableFolders = "1" +Revise = "3.11.0" StructIO = "0.3" julia = "1.10" diff --git a/src/compiling.jl b/src/compiling.jl index ebb0db4..88c3442 100644 --- a/src/compiling.jl +++ b/src/compiling.jl @@ -1,3 +1,14 @@ +# Helper to conditionally create and manage a temporary depot for trim preferences +# When trim is enabled, creates a temp depot and calls f(depot_path) with automatic cleanup. +# When trim is disabled, calls f(nothing) without creating a depot. +function mktempdepot(f, trim_enabled::Bool) + if trim_enabled + mktempdir(f) + else + f(nothing) + end +end + # Lightweight terminal spinner function _start_spinner(message::String; io::IO=stderr) anim_chars = ("◐", "◓", "◑", "◒") @@ -78,52 +89,92 @@ function compile_products(recipe::ImageRecipe) end project_arg = recipe.project == "" ? Base.active_project() : recipe.project - env_overrides = Dict{String,Any}("JULIA_LOAD_PATH"=>nothing) - inst_cmd = addenv(`$(Base.julia_cmd(cpu_target=precompile_cpu_target)) --project=$project_arg -e "using Pkg; Pkg.instantiate(); Pkg.precompile()"`, env_overrides...) - recipe.verbose && println("Running: $inst_cmd") - precompile_time = time_ns() - if !success(pipeline(inst_cmd; stdout, stderr)) - error("Error encountered during instantiate/precompile of app project.") - end - recipe.verbose && println("Precompilation took $((time_ns() - precompile_time)/1e9) s") - # Compile the Julia code - if recipe.img_path == "" - tmpdir = mktempdir() - recipe.img_path = joinpath(tmpdir, "image.o.a") - end - project_arg = recipe.project == "" ? Base.active_project() : recipe.project - # Build command incrementally to guarantee proper token separation - cmd = julia_cmd - cmd = `$cmd --project=$project_arg $(image_arg) $(recipe.img_path) --output-incremental=no` - for a in strip_args - cmd = `$cmd $a` - end - for a in recipe.julia_args - cmd = `$cmd $a` - end - cmd = `$cmd $(joinpath(JuliaC.SCRIPTS_DIR, "juliac-buildscript.jl")) --scripts-dir $(JuliaC.SCRIPTS_DIR) --source $(abspath(recipe.file)) $(recipe.output_type)` - if recipe.add_ccallables - cmd = `$cmd --compile-ccallable` - end - if recipe.use_loaded_libs - cmd = `$cmd --use-loaded-libs` - end - # Threading - cmd = addenv(cmd, env_overrides...) - recipe.verbose && println("Running: $cmd") - # Show a spinner while the compiler runs - spinner_done, spinner_task = _start_spinner("Compiling...") - compile_time = time_ns() - try - if !success(pipeline(cmd; stdout, stderr)) - error("Failed to compile $(recipe.file)") + # Use mktempdepot helper to conditionally manage temp depot and preferences environment. + # When trim is enabled, the helper creates a temp depot and passes it to the block. + # When trim is disabled, it passes nothing. + mktempdepot(is_trim_enabled(recipe)) do tmp_depot + env_overrides = Dict{String,Any}() + tmp_prefs_env = nothing + + # If a temporary depot was created, set JULIA_DEPOT_PATH and create preferences environment + if tmp_depot !== nothing + load_path_sep = Sys.iswindows() ? ";" : ":" + env_overrides["JULIA_DEPOT_PATH"] = tmp_depot * load_path_sep + + # Create a temporary environment with a LocalPreferences.toml that will be added to JULIA_LOAD_PATH + tmp_prefs_env = mktempdir() + # Write Project.toml with Preferences as a dependency so preferences are inherited + open(joinpath(tmp_prefs_env, "Project.toml"), "w") do io + println(io, "[deps]") + println(io, "Preferences = \"21216c6a-2e73-6563-6e65-726566657250\"") + end + # Write LocalPreferences.toml with the trim preferences + open(joinpath(tmp_prefs_env, "LocalPreferences.toml"), "w") do io + println(io, "[Preferences]") + println(io, "trim_enabled = true") + end + # Prepend the temp env to JULIA_LOAD_PATH; let Julia expand @ and @stdlib implicitly + env_overrides["JULIA_LOAD_PATH"] = tmp_prefs_env * load_path_sep + end + + try + inst_cmd = addenv(`$(Base.julia_cmd(cpu_target=precompile_cpu_target)) --project=$project_arg -e "using Pkg; Pkg.instantiate(); Pkg.precompile()"`, env_overrides...) + recipe.verbose && println("Running: $inst_cmd") + precompile_time = time_ns() + if !success(pipeline(inst_cmd; stdout, stderr)) + error("Error encountered during instantiate/precompile of app project.") + end + recipe.verbose && println("Precompilation took $((time_ns() - precompile_time)/1e9) s") + # Compile the Julia code + if recipe.img_path == "" + tmpdir = mktempdir() + recipe.img_path = joinpath(tmpdir, "image.o.a") + end + project_arg = recipe.project == "" ? Base.active_project() : recipe.project + # Build command incrementally to guarantee proper token separation + cmd = julia_cmd + cmd = `$cmd --project=$project_arg $(image_arg) $(recipe.img_path) --output-incremental=no` + for a in strip_args + cmd = `$cmd $a` + end + for a in recipe.julia_args + cmd = `$cmd $a` + end + cmd = `$cmd $(joinpath(JuliaC.SCRIPTS_DIR, "juliac-buildscript.jl")) --scripts-dir $(JuliaC.SCRIPTS_DIR) --source $(abspath(recipe.file)) $(recipe.output_type)` + if recipe.add_ccallables + cmd = `$cmd --compile-ccallable` + end + if recipe.use_loaded_libs + cmd = `$cmd --use-loaded-libs` + end + + # Threading + cmd = addenv(cmd, env_overrides...) + recipe.verbose && println("Running: $cmd") + # Show a spinner while the compiler runs + spinner_done, spinner_task = _start_spinner("Compiling...") + compile_time = time_ns() + try + if !success(pipeline(cmd; stdout, stderr)) + error("Failed to compile $(recipe.file)") + end + finally + spinner_done[] = true + wait(spinner_task) + end + recipe.verbose && println("Compilation took $((time_ns() - compile_time)/1e9) s") + finally + # Cleanup temporary preferences environment if created + if tmp_prefs_env !== nothing + try + rm(tmp_prefs_env; recursive=true, force=true) + catch e + @warn "Failed to cleanup temporary preferences environment: $tmp_prefs_env" exception=e + end + end end - finally - spinner_done[] = true - wait(spinner_task) end - recipe.verbose && println("Compilation took $((time_ns() - compile_time)/1e9) s") # Print compiled image size if recipe.verbose @assert isfile(recipe.img_path) diff --git a/test/TrimPrefsProject/Project.toml b/test/TrimPrefsProject/Project.toml new file mode 100644 index 0000000..009c624 --- /dev/null +++ b/test/TrimPrefsProject/Project.toml @@ -0,0 +1,10 @@ +name = "TrimPrefsProject" +uuid = "2685b477-6e63-411d-9143-facd9abab21c" +version = "0.1.0" +authors = ["Gabriel Baraldi "] + +[deps] +Preferences = "21216c6a-2e73-6563-6e65-726566657250" + +[compat] +Preferences = "1.5.0" diff --git a/test/TrimPrefsProject/src/TrimPrefsProject.jl b/test/TrimPrefsProject/src/TrimPrefsProject.jl new file mode 100644 index 0000000..77457c1 --- /dev/null +++ b/test/TrimPrefsProject/src/TrimPrefsProject.jl @@ -0,0 +1,18 @@ +module TrimPrefsProject + +using Preferences + +# Read the trim_enabled preference at compile time from the Preferences package +# This preference is set by JuliaC in the temporary depot during compilation +const TRIM_ENABLED = Preferences.load_preference(Preferences, "trim_enabled", false)::Bool + +function @main(ARGS) + if TRIM_ENABLED + println(Core.stdout, "TRIM_MODE_ENABLED") + else + println(Core.stdout, "TRIM_MODE_DISABLED") + end + return 0 +end + +end # module TrimPrefsProject diff --git a/test/programatic.jl b/test/programatic.jl index 86ef18e..51d1426 100644 --- a/test/programatic.jl +++ b/test/programatic.jl @@ -302,3 +302,27 @@ end # Print tree for debugging/inspection print_tree_with_sizes(outdir) end + +@testset "Trim preference propagation" begin + # Test that the trim_enabled preference is correctly propagated to packages + # during precompilation and compilation when trim mode is enabled. + # This verifies that LocalPreferences.toml is written and read correctly. + outdir = mktempdir() + exeout = joinpath(outdir, "trim_prefs_exe") + img = JuliaC.ImageRecipe( + file = TEST_TRIM_PREFS_PROJ, + output_type = "--output-exe", + trim_mode = "safe", + verbose = true, + ) + JuliaC.compile_products(img) + link = JuliaC.LinkRecipe(image_recipe=img, outname=exeout) + 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) + output = read(`$actual_exe`, String) + # When trim is enabled, the package should see trim_enabled = true + @test occursin("TRIM_MODE_ENABLED", output) +end diff --git a/test/runtests.jl b/test/runtests.jl index 4a214b3..8f6211c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,7 @@ const TEST_PROJ = abspath(joinpath(@__DIR__, "AppProject")) const TEST_SRC = joinpath(TEST_PROJ, "src", "test.jl") const TEST_LIB_PROJ = abspath(joinpath(@__DIR__, "lib_project")) const TEST_LIB_SRC = joinpath(TEST_LIB_PROJ, "src", "libtest.jl") +const TEST_TRIM_PREFS_PROJ = abspath(joinpath(@__DIR__, "TrimPrefsProject")) include("utils.jl") include("programatic.jl")