From ace033d8ceaa05211099bf7d0b32b9507fbb86d0 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Thu, 12 Mar 2026 14:20:33 +0000 Subject: [PATCH] Bundle GPU dylibs for portable macOS builds Add libepoxy, virglrenderer, and MoltenVK to the macOS dylib bundling pipeline alongside libkrun/libkrunfw. All dylibs get LC_ID_DYLIB rewritten to @loader_path, cross-library references patched, stale Homebrew rpaths removed, and ad-hoc codesigned. A verification step ensures no /opt/homebrew references remain. Extend RuntimeBundle() with a variadic extraLibs parameter so the extract layer can handle additional dylibs without breaking existing callers. Co-Authored-By: Claude Opus 4.6 --- Taskfile.yaml | 65 +++++++++++++++++++++++++++++++++++++++--- extract/source.go | 10 +++++-- extract/source_test.go | 36 +++++++++++++++++++++++ 3 files changed, 104 insertions(+), 7 deletions(-) diff --git a/Taskfile.yaml b/Taskfile.yaml index 493977a..8575634 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -83,20 +83,75 @@ tasks: platforms: [darwin] cmds: - task: build-dev-darwin + # Copy all required dylibs from Homebrew - cp -f $(brew --prefix libkrun)/lib/libkrun.{{.LIBKRUN_MAJOR}}.dylib bin/ - cp -f $(brew --prefix libkrunfw)/lib/libkrunfw.{{.LIBKRUNFW_MAJOR}}.dylib bin/ - # Rewrite absolute Homebrew install name to @loader_path for portable bundles + - cp -f $(brew --prefix libepoxy)/lib/libepoxy.0.dylib bin/ + - cp -f $(brew --prefix virglrenderer)/lib/libvirglrenderer.1.dylib bin/ + - cp -f $(brew --prefix molten-vk)/lib/libMoltenVK.dylib bin/ + # Rewrite LC_ID_DYLIB (each dylib's own identity) for portable bundles + - install_name_tool -id @loader_path/libkrun.{{.LIBKRUN_MAJOR}}.dylib bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib + - install_name_tool -id @loader_path/libkrunfw.{{.LIBKRUNFW_MAJOR}}.dylib bin/libkrunfw.{{.LIBKRUNFW_MAJOR}}.dylib + - install_name_tool -id @loader_path/libepoxy.0.dylib bin/libepoxy.0.dylib + - install_name_tool -id @loader_path/libvirglrenderer.1.dylib bin/libvirglrenderer.1.dylib + - install_name_tool -id @loader_path/libMoltenVK.dylib bin/libMoltenVK.dylib + # Rewrite propolis-runner → libkrun reference - >- install_name_tool -change /opt/homebrew/opt/libkrun/lib/libkrun.{{.LIBKRUN_MAJOR}}.dylib @loader_path/libkrun.{{.LIBKRUN_MAJOR}}.dylib bin/{{.RUNNER_NAME}} - # Re-sign after binary modification (install_name_tool invalidates signature) + # Rewrite libkrun → libepoxy, virglrenderer references + - >- + install_name_tool -change + /opt/homebrew/opt/libepoxy/lib/libepoxy.0.dylib + @loader_path/libepoxy.0.dylib + bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib + - >- + install_name_tool -change + /opt/homebrew/opt/virglrenderer/lib/libvirglrenderer.1.dylib + @loader_path/libvirglrenderer.1.dylib + bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib + # Rewrite virglrenderer → MoltenVK, libepoxy references + - >- + install_name_tool -change + /opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib + @loader_path/libMoltenVK.dylib + bin/libvirglrenderer.1.dylib + - >- + install_name_tool -change + /opt/homebrew/opt/libepoxy/lib/libepoxy.0.dylib + @loader_path/libepoxy.0.dylib + bin/libvirglrenderer.1.dylib + # Remove stale Homebrew rpaths (ignore errors if none exist) + - for f in bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib bin/libkrunfw.{{.LIBKRUNFW_MAJOR}}.dylib bin/libepoxy.0.dylib bin/libvirglrenderer.1.dylib bin/libMoltenVK.dylib; do + for rp in $(otool -l "$f" 2>/dev/null | grep -A2 LC_RPATH | grep 'path /opt/homebrew' | awk '{print $2}'); do + install_name_tool -delete_rpath "$rp" "$f" 2>/dev/null || true; + done; + done + # Code-sign dylibs ad-hoc (no entitlements), then runner last with entitlements + - codesign --force -s - bin/libMoltenVK.dylib + - codesign --force -s - bin/libepoxy.0.dylib + - codesign --force -s - bin/libvirglrenderer.1.dylib + - codesign --force -s - bin/libkrunfw.{{.LIBKRUNFW_MAJOR}}.dylib + - codesign --force -s - bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib - codesign --entitlements assets/entitlements.plist --force -s - bin/{{.RUNNER_NAME}} + # Verify no Homebrew references remain in bundled files + - | + for f in bin/{{.RUNNER_NAME}} bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib bin/libkrunfw.{{.LIBKRUNFW_MAJOR}}.dylib bin/libepoxy.0.dylib bin/libvirglrenderer.1.dylib bin/libMoltenVK.dylib; do + if otool -L "$f" | grep -q /opt/homebrew; then + echo "FAIL: $f still references /opt/homebrew" + otool -L "$f" | grep /opt/homebrew + exit 1 + fi + done generates: - bin/{{.RUNNER_NAME}} - bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib - bin/libkrunfw.{{.LIBKRUNFW_MAJOR}}.dylib + - bin/libepoxy.0.dylib + - bin/libvirglrenderer.1.dylib + - bin/libMoltenVK.dylib build-dev-race: desc: Build runner with race detector (requires libkrun-devel) @@ -265,7 +320,7 @@ tasks: rm -rf "${staging}" package-runtime-darwin: - desc: Package macOS runtime tarball (runner + libkrun) + desc: Package macOS runtime tarball (runner + libkrun + GPU dylibs) platforms: [darwin] vars: TAG: '{{.TAG | default .VERSION}}' @@ -274,7 +329,9 @@ tasks: - | staging="propolis-runtime-darwin-{{.HOST_ARCH}}" mkdir -p "${staging}" - cp bin/propolis-runner bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib "${staging}/" + cp bin/propolis-runner bin/libkrun.{{.LIBKRUN_MAJOR}}.dylib \ + bin/libepoxy.0.dylib bin/libvirglrenderer.1.dylib bin/libMoltenVK.dylib \ + "${staging}/" echo "{{.TAG}}" > "${staging}/VERSION" tar czf "dist/${staging}.tar.gz" "${staging}" rm -rf "${staging}" diff --git a/extract/source.go b/extract/source.go index 270e5ae..259d23c 100644 --- a/extract/source.go +++ b/extract/source.go @@ -62,11 +62,15 @@ func (s *dirSource) Ensure(_ context.Context, _ string) (string, error) { // into a versioned cache directory. The runner and libkrun byte slices are // the file contents to extract. The libkrun major soname version is always 1 // because the runner binary is built against a specific libkrun ABI. -func RuntimeBundle(version string, runner, libkrun []byte) Source { - return &bundleSource{bundle: NewBundle(version, []File{ +// Additional dylibs (e.g. libepoxy, virglrenderer, MoltenVK on macOS) can be +// passed via extraLibs and will be extracted alongside the core files. +func RuntimeBundle(version string, runner, libkrun []byte, extraLibs ...File) Source { + files := []File{ {Name: RunnerBinaryName, Content: runner, Mode: 0o755}, {Name: LibName("krun", 1), Content: libkrun, Mode: 0o755}, - })} + } + files = append(files, extraLibs...) + return &bundleSource{bundle: NewBundle(version, files)} } // FirmwareBundle creates a Source that extracts libkrunfw into a versioned diff --git a/extract/source_test.go b/extract/source_test.go index 87e4097..458ffd5 100644 --- a/extract/source_test.go +++ b/extract/source_test.go @@ -155,6 +155,42 @@ func TestBundleSource_EmptyCacheDir(t *testing.T) { assert.Contains(t, err.Error(), "cache directory must not be empty") } +func TestRuntimeBundle_ExtraLibs(t *testing.T) { + t.Parallel() + + cacheDir := t.TempDir() + runnerData := []byte("runner-binary") + libkrunData := []byte("libkrun-data") + epoxyData := []byte("libepoxy-data") + virglData := []byte("virgl-data") + + src := RuntimeBundle("v1.0.0", runnerData, libkrunData, + File{Name: "libepoxy.0.dylib", Content: epoxyData, Mode: 0o755}, + File{Name: "libvirglrenderer.1.dylib", Content: virglData, Mode: 0o755}, + ) + + dir, err := src.Ensure(context.Background(), cacheDir) + require.NoError(t, err) + + // Verify core files still present. + got, err := os.ReadFile(filepath.Join(dir, RunnerBinaryName)) + require.NoError(t, err) + assert.Equal(t, runnerData, got) + + got, err = os.ReadFile(filepath.Join(dir, LibName("krun", 1))) + require.NoError(t, err) + assert.Equal(t, libkrunData, got) + + // Verify extra libs. + got, err = os.ReadFile(filepath.Join(dir, "libepoxy.0.dylib")) + require.NoError(t, err) + assert.Equal(t, epoxyData, got) + + got, err = os.ReadFile(filepath.Join(dir, "libvirglrenderer.1.dylib")) + require.NoError(t, err) + assert.Equal(t, virglData, got) +} + func TestRuntimeBundle_ConcurrentEnsure(t *testing.T) { t.Parallel()