feat(pkg): phase 15 - a package can carry a native library
native "<target>" "<path>" in a package's package.ludic; the compiler records the libraries of every package a program imports and writes them into the IR (; ludic-native:), so ludicc -o, ludic build, ludic test and ludic bundle all link one list. macOS: an rpath to the package and to Contents/Frameworks, where ludic bundle copies and signs each library and drops the build machine's rpath. Windows: the import library, the .dll copied beside the exe (--natives-out for the bundle). tools/native/lib.sh builds from a pinned, checksummed source with clang on both machines; ludic.nativeecho is the worked example; the shim rules are in packages/README.md. Linked at build time rather than dlopen (docs/PACKAGES.md says why). Reseeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5df0747642
commit
5d3a799e33
25 changed files with 54474 additions and 51707 deletions
|
|
@ -27,6 +27,8 @@ function ensure_ludicc() -> void {
|
|||
# rendered to build/out.ppm from piped input — deterministic output for tests).
|
||||
# Returns true on success. `ludic build`, `ludic run` and the contributor tool all
|
||||
# go through here.
|
||||
# the native libraries (phase 15) the last compile_app linked, for `ludic bundle` to place
|
||||
var g_built_natives: []pointer = new []pointer
|
||||
function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool {
|
||||
ensure_ludicc() # in a checkout: assemble the seed if bin/ludicc is missing or stale
|
||||
let home = ludic_home()
|
||||
|
|
@ -52,7 +54,8 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
|
||||
if mode == 2 {
|
||||
if not shq(`{ludicc()} --headless{title_flag()}{unsafe_flag()} {src} --emit-llvm -o {ll}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
g_built_natives = native_libs_of(ll)
|
||||
if not save { shell(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
|
@ -62,7 +65,8 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
# canonical `ludicc -o` path links it only when Audio.* is used.
|
||||
let cocoa = `{home}runtime/native/cocoa.ll`
|
||||
let audio = `{home}runtime/native/audio.ll`
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
g_built_natives = native_libs_of(ll)
|
||||
if not save { shell(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
|
@ -105,6 +109,30 @@ function vk_link_flags(ll: pointer) -> pointer {
|
|||
return ` {home}runtime/native/vk_thunks.ll {home}runtime/native/vk_mac.ll`
|
||||
}
|
||||
|
||||
# A package that carries a native library (phase 15) is written into the IR by the compiler as
|
||||
# `; ludic-native: <path>` lines: link each, with an rpath to its directory for a run from the
|
||||
# build and one to Contents/Frameworks, where `ludic bundle` puts it.
|
||||
function native_libs_of(ll: pointer) -> []pointer {
|
||||
let out = new []pointer
|
||||
let lines = split_lines(capture(`grep "^; ludic-native: " {ll} 2>/dev/null`))
|
||||
var i = 0
|
||||
while i < len(lines) {
|
||||
if s_starts(lines[i], "; ludic-native: ") { push(out, sslice(lines[i], 16, slen(lines[i]))) }
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
function native_link_flags(ll: pointer) -> pointer {
|
||||
let libs = native_libs_of(ll)
|
||||
var f = ""
|
||||
var i = 0
|
||||
while i < len(libs) {
|
||||
f = f + ` "{libs[i]}" -Wl,-rpath,{dir_of_path(libs[i])} -Wl,-rpath,@executable_path/../Frameworks`
|
||||
i += 1
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
# A program that uses Http.* calls the @hs_* transport; link http.ll and Foundation only
|
||||
# then, in both modes, as `ludicc -o` does (selfhost/main.ludic). Without it a Http.*
|
||||
# program built through `ludic build` failed to link on every hs_* symbol.
|
||||
|
|
|
|||
|
|
@ -272,6 +272,35 @@ function windows_icon_res(icon: pointer) -> pointer {
|
|||
return res
|
||||
}
|
||||
|
||||
# copy what the last build linked into Contents/Frameworks
|
||||
# and drop the rpath to where each was built: a shipped binary names no path on this machine
|
||||
function bundle_natives_mac(contents: pointer, exe: pointer) -> bool {
|
||||
if len(g_built_natives) == 0 { return true }
|
||||
shell(`mkdir -p "{contents}/Frameworks"`)
|
||||
for i in 0 .. len(g_built_natives) {
|
||||
shq(`install_name_tool -delete_rpath "{dir_of_path(g_built_natives[i])}" "{exe}" 2>/dev/null`)
|
||||
if not shq(`cp "{g_built_natives[i]}" "{contents}/Frameworks/"`) {
|
||||
err(`ludic bundle: could not place {g_built_natives[i]}\n`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
# copy the .dll files ludicc wrote into <exe>.natives beside the executable
|
||||
function bundle_natives_win(list: pointer, root: pointer) -> bool {
|
||||
let txt = read_file(list)
|
||||
if txt == null { return true }
|
||||
let libs = split_lines(txt)
|
||||
for i in 0 .. len(libs) {
|
||||
if libs[i] != "" and not shq(`cp "{libs[i]}" "{root}/"`) {
|
||||
err(`ludic bundle: could not place {libs[i]}\n`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function cmd_bundle_windows() -> int {
|
||||
let entry = parse_build_args(2)
|
||||
if g_argerr { return 1 }
|
||||
|
|
@ -288,11 +317,12 @@ function cmd_bundle_windows() -> int {
|
|||
let staged = tmp_path("bundle_exe")
|
||||
var flags = "--windowed --gui"
|
||||
if res != "" { flags = flags + ` --link "{res}"` }
|
||||
if not shq(`{ludicc()} {flags}{title_flag()} {entry} -o {staged}`) { return 1 }
|
||||
if not shq(`{ludicc()} {flags}{title_flag()} {entry} --natives-out {staged}.natives -o {staged}`) { return 1 }
|
||||
if not shq(`cp "{staged}.exe" "{root}/{name}.exe"`) {
|
||||
err("ludic bundle: could not place the executable\n")
|
||||
return 1
|
||||
}
|
||||
if not bundle_natives_win(`{staged}.natives`, root) { return 1 }
|
||||
|
||||
# 2. the assets, exactly as the .app packs them
|
||||
let roots = pack_roots(m, 999)
|
||||
|
|
@ -376,6 +406,11 @@ function cmd_bundle() -> int {
|
|||
return 1
|
||||
}
|
||||
|
||||
# 1b. a package's native libraries (phase 15) go in Contents/Frameworks, where the binary's
|
||||
# @executable_path/../Frameworks rpath finds them - never in MacOS, which must hold mach-o
|
||||
# executables only (a Velopack update strips anything else's detached signature)
|
||||
if not bundle_natives_mac(contents, exe) { return 1 }
|
||||
|
||||
# 2. the assets
|
||||
let roots = pack_roots(m, 999) # manifest/convention only, no argv
|
||||
var packed = false
|
||||
|
|
@ -430,6 +465,13 @@ function cmd_bundle() -> int {
|
|||
# ID and notarisation - but it does make it run here.
|
||||
var ident = manifest_app(m, "sign")
|
||||
if ident == "" { ident = "-" }
|
||||
# nested code is signed before what contains it
|
||||
for i in 0 .. len(g_built_natives) {
|
||||
let fw = `{contents}/Frameworks/{file_of_path(g_built_natives[i])}`
|
||||
if not shq(`codesign --force --timestamp=none --sign "{ident}" "{fw}" 2>/dev/null`) {
|
||||
err(`ludic bundle: warning: codesign failed on {fw}\n`)
|
||||
}
|
||||
}
|
||||
if not shq(`codesign --force --timestamp=none --sign "{ident}" "{root}" 2>/dev/null`) {
|
||||
err(`ludic bundle: warning: codesign failed; the app may not launch\n`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/generics.ludic")
|
||||
push(f, "selfhost/frontend/aliases.ludic")
|
||||
push(f, "selfhost/frontend/modules.ludic")
|
||||
push(f, "selfhost/frontend/natives.ludic")
|
||||
push(f, "selfhost/frontend/ports.ludic")
|
||||
push(f, "selfhost/frontend/privates.ludic")
|
||||
push(f, "selfhost/frontend/privates_types.ludic")
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function test_parallel(files: []pointer, only: pointer, verbose: bool) -> int {
|
|||
let bin = bins[i]
|
||||
let ll = `{bin}.ll`
|
||||
if test_rc(`{bin}.rc1`) == 0 {
|
||||
let flags = `{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf}`
|
||||
let flags = `{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf}`
|
||||
write_file(`{jd}/l{string(i)}.sh`, `{cc()} -O2 {ll}{flags} -o {bin} >> {bin}.log 2>&1; echo $? > {bin}.rc2\n`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
tools/native/lib.sh
Normal file
81
tools/native/lib.sh
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# tools/native/lib.sh - what every package's native/build.sh shares (phase 15). Sourced, not run.
|
||||
#
|
||||
# A native library is built here from a pinned source, never downloaded as a binary: the upstream
|
||||
# tag is fetched, its SHA-256 checked, and it and the package's shim compiled with clang into
|
||||
# lib/<target>/. The same script runs on the Mac (Terminal) and on the PC (Git Bash, with the LLVM
|
||||
# installer's clang), so there is one recipe per library, not two.
|
||||
set -eu
|
||||
|
||||
# macos-arm64, macos-x64, windows-x64 or linux-x64 - the names `native` lines use
|
||||
native_target() {
|
||||
if [ -n "${LUDIC_NATIVE_TARGET:-}" ]; then echo "$LUDIC_NATIVE_TARGET"; return; fi
|
||||
case "$(uname -s)" in
|
||||
Darwin) if [ "$(uname -m)" = arm64 ]; then echo macos-arm64; else echo macos-x64; fi ;;
|
||||
MINGW*|MSYS*|CYGWIN*) echo windows-x64 ;;
|
||||
*) echo linux-x64 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# the clang that builds: $LUDIC_CC, else clang on PATH, else the LLVM installer's on Windows
|
||||
native_cc() {
|
||||
if [ -n "${LUDIC_CC:-}" ]; then echo "$LUDIC_CC"; return; fi
|
||||
if command -v clang >/dev/null 2>&1; then echo clang; return; fi
|
||||
if [ -x "/c/Program Files/LLVM/bin/clang.exe" ]; then echo "/c/Program Files/LLVM/bin/clang.exe"; return; fi
|
||||
echo "native: no clang (set LUDIC_CC)" >&2; exit 1
|
||||
}
|
||||
native_cxx() {
|
||||
local cc; cc="$(native_cc)"
|
||||
case "$cc" in *clang.exe) echo "${cc%clang.exe}clang++.exe" ;; *clang) echo "${cc}++" ;; *) echo "$cc" ;; esac
|
||||
}
|
||||
|
||||
native_sha256() {
|
||||
if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
|
||||
else sha256sum "$1" | cut -d' ' -f1; fi
|
||||
}
|
||||
|
||||
# native_fetch <dir> <url> <sha256>: the archive at url, checked, unpacked into <dir> (its one
|
||||
# top-level directory stripped). A checksum that does not match stops the build.
|
||||
native_fetch() {
|
||||
local dir="$1" url="$2" want="$3"
|
||||
local tgz="$dir.tar.gz"
|
||||
mkdir -p "$(dirname "$dir")"
|
||||
if [ ! -f "$tgz" ]; then curl -fsSL "$url" -o "$tgz.part" && mv "$tgz.part" "$tgz"; fi
|
||||
local got; got="$(native_sha256 "$tgz")"
|
||||
if [ "$got" != "$want" ]; then
|
||||
echo "native: $url is $got, pinned $want" >&2; rm -f "$tgz"; exit 1
|
||||
fi
|
||||
if [ ! -d "$dir" ]; then
|
||||
mkdir -p "$dir.part" && tar -xzf "$tgz" -C "$dir.part" --strip-components 1 && mv "$dir.part" "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
# native_link <package dir> <name> <objects...>: lib<name>.dylib (install name @rpath, so the
|
||||
# executable finds it beside itself, in Contents/Frameworks or in lib/<target> while developing)
|
||||
# or <name>.dll with its import library <name>.lib, into <package>/lib/<target>/
|
||||
native_link() {
|
||||
local pkg="$1" name="$2"; shift 2
|
||||
local t; t="$(native_target)"
|
||||
local out="$pkg/lib/$t"
|
||||
mkdir -p "$out"
|
||||
local cxx; cxx="$(native_cxx)"
|
||||
case "$t" in
|
||||
macos-*)
|
||||
"$cxx" -dynamiclib -O2 -Wl,-install_name,@rpath/lib$name.dylib -Wl,-dead_strip "$@" -o "$out/lib$name.dylib"
|
||||
echo "native: $out/lib$name.dylib" ;;
|
||||
windows-*)
|
||||
"$cxx" -shared -O2 "$@" -o "$out/$name.dll" -Wl,/implib:"$out/$name.lib"
|
||||
echo "native: $out/$name.dll + $name.lib" ;;
|
||||
*)
|
||||
"$cxx" -shared -O2 -fPIC -Wl,-soname,lib$name.so "$@" -o "$out/lib$name.so"
|
||||
echo "native: $out/lib$name.so" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# the flags every object is compiled with: optimised, position independent, symbols hidden unless
|
||||
# the shim exports them
|
||||
native_cflags() {
|
||||
case "$(native_target)" in
|
||||
windows-*) echo "-O2" ;;
|
||||
*) echo "-O2 -fPIC -fvisibility=hidden" ;;
|
||||
esac
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue