Skip to content

Commit 5714bab

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 5714bab

File tree

6 files changed

+152
-12
lines changed

6 files changed

+152
-12
lines changed
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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
function _build_external_dylib()
2+
local arch = os.arch()
3+
local sdkdir = os.iorunv("xcrun", {"--sdk", "macosx", "--show-sdk-path"}):trim()
4+
os.execv("xcrun", {
5+
"--sdk", "macosx", "clang",
6+
"-dynamiclib",
7+
"-target", arch .. "-apple-macos",
8+
"-isysroot", sdkdir,
9+
"-install_name", "@rpath/libextfoo.dylib",
10+
"-o", "ext/libextfoo.dylib",
11+
"ext/extfoo.c"
12+
})
13+
end
14+
15+
function main(t)
16+
if not is_host("macosx") then
17+
return t:skip("wrong host platform")
18+
end
19+
20+
local homedir = path.absolute("home")
21+
os.setenv("HOME", homedir)
22+
os.mkdir(homedir)
23+
os.mkdir(path.join(homedir, ".xmake"))
24+
25+
_build_external_dylib()
26+
local arch = os.arch()
27+
28+
local xmake = path.absolute(path.join(os.projectdir(), "build", "xmake"))
29+
local xmake_program_dir = path.absolute(path.join(os.projectdir(), "xmake"))
30+
os.setenv("XMAKE_PROGRAM_FILE", xmake)
31+
os.setenv("XMAKE_PROGRAM_DIR", xmake_program_dir)
32+
33+
os.execv(xmake, {"f", "-p", "macosx", "-a", arch, "-c"})
34+
os.execv(xmake, {"-vD"})
35+
36+
local appdir = path.join("build", "macosx", arch, "release", "demo.app", "Contents", "Frameworks")
37+
local dylibfile = path.join(appdir, "libextfoo.dylib")
38+
if not os.isfile(dylibfile) then
39+
raise("missing external dylib in macOS app bundle: %s", dylibfile)
40+
end
41+
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: 73 additions & 12 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,46 @@ 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 frameworks_to_copy = {}
90+
local framework_targetfiles = {}
5591
for _, dep in ipairs(target:orderdeps()) do
56-
if dep:kind() == "shared" then
57-
if not os.isdir(frameworksdir) then
58-
os.mkdir(frameworksdir)
59-
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)
65-
end
92+
local frameworkdir = dep:data("xcode.bundle.rootdir")
93+
if dep:rule("xcode.framework") and frameworkdir then
94+
table.insert(frameworks_to_copy, frameworkdir)
95+
framework_targetfiles[path.absolute(dep:targetfile())] = true
96+
end
97+
end
98+
local libfiles = {}
99+
target_utils.get_target_libfiles(target, libfiles, target:targetfile(), {})
100+
table.join2(libfiles, _get_target_linklibfiles(target))
101+
local dependfiles = get_depend_libraries(target:targetfile(), {
102+
plat = target:plat(),
103+
arch = target:arch(),
104+
recursive = true,
105+
resolve_path = true,
106+
resolve_hint_paths = libfiles
107+
})
108+
for _, dependfile in ipairs(table.wrap(dependfiles)) do
109+
if _is_non_system_dylib(dependfile) then
110+
table.insert(libfiles, dependfile)
111+
end
112+
end
113+
local dylibs_to_copy = {}
114+
for _, libfile in ipairs(table.unique(libfiles)) do
115+
if not framework_targetfiles[path.absolute(libfile)] then
116+
table.insert(dylibs_to_copy, libfile)
117+
end
118+
end
119+
if #frameworks_to_copy > 0 or #dylibs_to_copy > 0 then
120+
if not os.isdir(frameworksdir) then
121+
os.mkdir(frameworksdir)
122+
end
123+
for _, frameworkdir in ipairs(frameworks_to_copy) do
124+
os.cp(frameworkdir, frameworksdir, {symlink = true})
125+
end
126+
for _, libfile in ipairs(dylibs_to_copy) do
127+
os.vcp(libfile, frameworksdir)
66128
end
67129
end
68130

@@ -109,4 +171,3 @@ function main (target, opt)
109171

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

0 commit comments

Comments
 (0)