Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,6 +25,7 @@ Patchelf_jll = "0.18"
Pkg = "1"
RelocatableFolders = "1"
StructIO = "0.3"
TOML = "1"
julia = "1.10"

[apps.juliac]
Expand Down
1 change: 1 addition & 0 deletions src/JuliaC.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ using Pkg
using PackageCompiler
using LazyArtifacts
using RelocatableFolders
using TOML

@static if VERSION >= v"1.12.0-rc1"

Expand Down
50 changes: 33 additions & 17 deletions src/compiling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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...)
Expand Down
9 changes: 9 additions & 0 deletions test/DepProject/Project.toml
Original file line number Diff line number Diff line change
@@ -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 = {}
11 changes: 11 additions & 0 deletions test/DepProject/src/DepProject.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module DepProject

using SHA

function @main(ARGS)
h = bytes2hex(sha256("hello"))
println(Core.stdout, "sha256: ", h)
return 0
end

end
67 changes: 67 additions & 0 deletions test/cli.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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=<JuliaC project>/. 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
50 changes: 50 additions & 0 deletions test/programatic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading