Skip to content

Commit 96b07e4

Browse files
committed
fix(xcode.application): bundle external dylibs in macOS apps
Collect non-target shared library dependencies for macOS app bundles in addition to shared target and framework dependencies. Resolve declared shared links against merged linkdirs, reuse existing target/package library collection, and then walk the app binary's dylib dependencies to copy non-system dylibs into Contents/Frameworks. Add a regression test that builds an external dylib and verifies xcode.application bundles it into a macOS app.
1 parent 39fd35e commit 96b07e4

6 files changed

Lines changed: 139 additions & 8 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int extfoo_value(void) {
2+
return 7;
3+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleExecutable</key>
6+
<string>demo</string>
7+
<key>CFBundleIdentifier</key>
8+
<string>io.xmake.demo.external</string>
9+
<key>CFBundleName</key>
10+
<string>demo</string>
11+
<key>CFBundlePackageType</key>
12+
<string>APPL</string>
13+
<key>CFBundleVersion</key>
14+
<string>1</string>
15+
<key>CFBundleShortVersionString</key>
16+
<string>1.0</string>
17+
</dict>
18+
</plist>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <AppKit/AppKit.h>
2+
3+
int extfoo_value(void);
4+
5+
int main(void) {
6+
return extfoo_value() == 7 ? 0 : 1;
7+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
function _build_external_dylib()
2+
local sdkdir = os.iorunv("xcrun", {"--sdk", "macosx", "--show-sdk-path"}):trim()
3+
os.execv("xcrun", {
4+
"--sdk", "macosx", "clang",
5+
"-dynamiclib",
6+
"-target", "arm64-apple-macos14.2",
7+
"-isysroot", sdkdir,
8+
"-install_name", "@rpath/libextfoo.dylib",
9+
"-o", "ext/libextfoo.dylib",
10+
"ext/extfoo.c"
11+
})
12+
end
13+
14+
function main(t)
15+
if not is_host("macosx") then
16+
return t:skip("wrong host platform")
17+
end
18+
19+
local homedir = path.absolute("home")
20+
os.setenv("HOME", homedir)
21+
os.mkdir(homedir)
22+
os.mkdir(path.join(homedir, ".xmake"))
23+
24+
_build_external_dylib()
25+
26+
local xmake = path.absolute(path.join(os.projectdir(), "build", "xmake"))
27+
local xmake_program_dir = path.absolute(path.join(os.projectdir(), "xmake"))
28+
os.setenv("XMAKE_PROGRAM_FILE", xmake)
29+
os.setenv("XMAKE_PROGRAM_DIR", xmake_program_dir)
30+
31+
os.execv(xmake, {"f", "-p", "macosx", "-a", "arm64", "-c"})
32+
os.execv(xmake, {"-vD"})
33+
34+
local appdir = "build/macosx/arm64/release/demo.app/Contents/Frameworks"
35+
local dylibfile = path.join(appdir, "libextfoo.dylib")
36+
if not os.isfile(dylibfile) then
37+
raise("missing external dylib in macOS app bundle: %s", dylibfile)
38+
end
39+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
add_rules("mode.release", "mode.debug")
2+
3+
set_languages("c11", "objc")
4+
5+
target("demo")
6+
add_rules("xcode.application")
7+
add_linkdirs("ext")
8+
add_links("extfoo")
9+
add_files("src/main.m")
10+
add_files("src/Info.plist")

xmake/rules/xcode/application/build.lua

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,42 @@
2222
import("core.base.option")
2323
import("core.theme.theme")
2424
import("core.project.depend")
25+
import("lib.detect.find_library")
2526
import("private.tools.codesign")
27+
import("private.utils.target", {alias = "target_utils"})
28+
import("utils.binary.deplibs", {alias = "get_depend_libraries"})
2629
import("utils.progress")
30+
31+
local function _is_non_system_dylib(libfile)
32+
return libfile and libfile:endswith(".dylib")
33+
and not libfile:startswith("/usr/lib/")
34+
and not libfile:startswith("/System/Library/")
35+
end
36+
37+
local function _get_target_linkdirs(target)
38+
local linkdirs = {}
39+
for _, values in ipairs(table.wrap(target:get_from("linkdirs", "*"))) do
40+
for _, linkdir in ipairs(table.wrap(values)) do
41+
table.insert(linkdirs, path.absolute(linkdir))
42+
end
43+
end
44+
return table.unique(linkdirs)
45+
end
46+
47+
local function _get_target_linklibfiles(target)
48+
local linkdirs = _get_target_linkdirs(target)
49+
local libfiles = {}
50+
for _, values in ipairs(table.wrap(target:get_from("links", "*"))) do
51+
for _, link in ipairs(table.wrap(values)) do
52+
local libinfo = find_library(link, linkdirs, {plat = target:plat(), kind = "shared"})
53+
if libinfo then
54+
table.insert(libfiles, path.join(libinfo.linkdir, libinfo.filename))
55+
end
56+
end
57+
end
58+
return table.unique(libfiles)
59+
end
60+
2761
function main (target, opt)
2862

2963
-- get app and resources directory
@@ -51,18 +85,39 @@ function main (target, opt)
5185
try { function () os.vrunv("install_name_tool", {"-delete_rpath", "@loader_path", targetfile}) end }
5286
os.vrunv("install_name_tool", {"-add_rpath", "@executable_path/../Frameworks", targetfile})
5387

54-
-- copy dependent dynamic libraries and frameworks
88+
-- copy dependent frameworks and dynamic libraries
89+
local framework_targetfiles = {}
5590
for _, dep in ipairs(target:orderdeps()) do
56-
if dep:kind() == "shared" then
91+
local frameworkdir = dep:data("xcode.bundle.rootdir")
92+
if dep:rule("xcode.framework") and frameworkdir then
5793
if not os.isdir(frameworksdir) then
5894
os.mkdir(frameworksdir)
5995
end
60-
local frameworkdir = dep:data("xcode.bundle.rootdir")
61-
if dep:rule("xcode.framework") and frameworkdir then
62-
os.cp(frameworkdir, frameworksdir, {symlink = true})
63-
else
64-
os.vcp(dep:targetfile(), frameworksdir)
96+
os.cp(frameworkdir, frameworksdir, {symlink = true})
97+
framework_targetfiles[path.absolute(dep:targetfile())] = true
98+
end
99+
end
100+
local libfiles = {}
101+
target_utils.get_target_libfiles(target, libfiles, target:targetfile(), {})
102+
table.join2(libfiles, _get_target_linklibfiles(target))
103+
local dependfiles = get_depend_libraries(target:targetfile(), {
104+
plat = target:plat(),
105+
arch = target:arch(),
106+
recursive = true,
107+
resolve_path = true,
108+
resolve_hint_paths = libfiles
109+
})
110+
for _, dependfile in ipairs(table.wrap(dependfiles)) do
111+
if _is_non_system_dylib(dependfile) then
112+
table.insert(libfiles, dependfile)
113+
end
114+
end
115+
for _, libfile in ipairs(table.unique(libfiles)) do
116+
if not framework_targetfiles[path.absolute(libfile)] then
117+
if not os.isdir(frameworksdir) then
118+
os.mkdir(frameworksdir)
65119
end
120+
os.vcp(libfile, frameworksdir)
66121
end
67122
end
68123

@@ -109,4 +164,3 @@ function main (target, opt)
109164

110165
end, {dependfile = target:dependfile(bundledir), files = {bundledir, target:targetfile()}, changed = target:is_rebuilt()})
111166
end
112-

0 commit comments

Comments
 (0)