Skip to content

Commit 420d8fc

Browse files
mattleibowCopilot
andcommitted
Migrate Apple native builds from Xcode projects to GN
Replace the hand-maintained Xcode projects for libSkiaSharp and libHarfBuzzSharp with the existing GN `skiasharp_build` targets, matching how Linux, Windows, WASM and Android already build. - Delete all 6 *.xcodeproj bundles (ios/tvos/macos × both libs) and scripts/infra/native/apple/xcode.cake. - Rewrite native/{ios,tvos,macos}/build.cake to drive GN/ninja only. - Add scripts/infra/native/apple/apple.cake (CombineFrameworks, StripSign, RunLipo, RunZip) for the post-GN lipo/strip/codesign/zip steps. - macOS ships a plain fat .dylib (@rpath/lib<N>.dylib); iOS/tvOS/MacCatalyst frameworks are emitted directly by GN with link-time framework-relative install_name and provenance-complete Info.plist; Cake only lipos per-arch frameworks together and strips/codesigns last. - Bump externals/skia to pull in the GN-emits-framework support. - Document the new flow in update-skia known-gotchas (#23). Verified against the released 3.119.0 baseline on all four Apple platforms (symbol counts 887 sk_/gr_ + 560 hb_, fat archs, framework-relative install_name, valid codesign, Info.plist key parity). iOS simulator and Mac Catalyst test suites pass 5548/0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e3bca18 commit 420d8fc

22 files changed

Lines changed: 315 additions & 2857 deletions

File tree

.agents/skills/update-skia/references/known-gotchas.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,47 @@ The C API shims (`src/c/gr_context.cpp` etc.) compile as part of `:core`, but ba
6161

6262
Upstream may move previously-core modules into separate optional targets. If the C API exposes functions from that module, add it as an explicit dependency of the `SkiaSharp` target in `BUILD.gn` rather than merging sources into core.
6363

64+
### 23. Apple Builds Are Pure GN — GN Emits the `.framework`
65+
66+
Every platform — Windows, Linux, WASM, Android, macOS, iOS, tvOS, and MacCatalyst — builds
67+
`libSkiaSharp`/`libHarfBuzzSharp` from the `skiasharp_build("SkiaSharp")` /
68+
`skiasharp_build("HarfBuzzSharp")` GN targets in `externals/skia/BUILD.gn`. There are **no Xcode
69+
project files** — the Apple `native/{ios,tvos,macos}/build.cake` tasks drive `gn`/`ninja` directly,
70+
exactly like the other platforms.
71+
72+
On iOS/tvOS/MacCatalyst the GN build itself emits a complete single-arch `lib<Name>.framework` in
73+
its out dir — bundle layout, framework-relative install_name (set via link-time ldflags), arm64e
74+
thinning, and the provenance Info.plist (CFBundle* + DT*/BuildMachineOSBuild keys App Store /
75+
notarization validation expects) all come from GN. The framework path is keyed entirely off the
76+
**OS** — there are no separate `skiasharp_apple_framework*` GN args: the `skiasharp_build` template
77+
in `gn/BUILDCONFIG.gn` emits a framework whenever `is_ios` is true (iOS/tvOS/MacCatalyst), uses the
78+
macOS-style versioned `Versions/A` layout when `is_maccatalyst`, and the install_name ldflags in
79+
`BUILD.gn` are gated the same way. The single GN `action` runs
80+
`gn/skiasharp/assemble_apple_framework.py` (self-contained Python — it shells out only to first-party
81+
Apple tools `lipo`/`xcrun`/`xcodebuild`/`sw_vers` and writes the plist with `plistlib`). The cake
82+
`CombineFrameworks` helper (`scripts/infra/native/apple/apple.cake`) then only lipos the per-arch
83+
frameworks together and code-signs last. macOS ships a plain fat `.dylib` (install_name
84+
`@rpath/libSkiaSharp.dylib`, set by GN's solink rule) and does not call `CombineFrameworks`.
85+
86+
What to do when updating Skia:
87+
88+
- **Adding/removing a C API source file** (`src/c/*.cpp`): update the list in `gn/core.gni` — these
89+
shims compile into `:core` (see gotcha #21), which `skiasharp_build` reaches via `:skia`, so the
90+
same list feeds desktop, mobile, and Apple builds. `src/xamarin/*.cpp` sources live directly on the
91+
`skiasharp_build("SkiaSharp")` target in `BUILD.gn`.
92+
- **Adding/removing a HarfBuzz source file**: update the `skiasharp_build("HarfBuzzSharp")` target.
93+
Keep its defines (`HAVE_OT`, `HAVE_CONFIG_OVERRIDE_H`, `HB_NO_FALLBACK_SHAPE`) and the `HB_EXTERN`
94+
visibility export (which publishes the `hb_*` symbols) intact.
95+
- **Changing a feature define or warning flag** (e.g. `SK_*`): set it on the GN target/config and it
96+
applies everywhere, Apple included.
97+
- After any native change, rebuild from source per platform (`dotnet cake --target=externals-<plat>`);
98+
`externals-download` is forbidden for native work.
99+
100+
A full Xcode install is still required on macOS build agents (not just the Command Line
101+
Tools): GN drives the Apple SDK + clang toolchain, and the framework-assembly action shells
102+
out to `xcodebuild -version` and `xcrun --show-sdk-version` for the provenance `Info.plist`,
103+
which need a full, license-accepted Xcode. The build itself is otherwise pure `gn`/`ninja`.
104+
64105
## Dependencies & Bindings
65106

66107
### 8. DEPS: Fork-Customized Dependencies

build.cake

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ Task ("clean-externals")
184184
.Does (() =>
185185
{
186186
CleanDirectories("externals/skia/out");
187-
CleanDirectories("externals/skia/xcodebuild");
188187
CleanDirectories("externals/angle");
189188
CleanDirectories("output/native");
190189
CleanDirectories("native/*/*/bin");

native/ios/build.cake

Lines changed: 70 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ DirectoryPath ROOT_PATH = MakeAbsolute(Directory("../.."));
22
DirectoryPath OUTPUT_PATH = MakeAbsolute(ROOT_PATH.Combine("output/native"));
33

44
#load "../../scripts/infra/native/shared/native-shared.cake"
5-
#load "../../scripts/infra/native/apple/xcode.cake"
5+
#load "../../scripts/infra/native/apple/apple.cake"
66

77
string VARIANT = (BUILD_VARIANT ?? "ios").ToLower();
88

@@ -14,109 +14,115 @@ string GetDeploymentTarget(string arch)
1414
}
1515
}
1616

17+
string SkiaGnArgs(string skiaArch, string arch, bool isSim) =>
18+
$"target_cpu='{skiaArch}' " +
19+
$"target_os='{VARIANT}' " +
20+
$"min_{VARIANT}_version='{GetDeploymentTarget(arch)}' " +
21+
$"ios_use_simulator={(isSim ? "true" : "false")} " +
22+
$"skia_use_harfbuzz=false " +
23+
$"skia_use_icu=false " +
24+
$"skia_use_metal=true " +
25+
$"skia_use_piex=true " +
26+
$"skia_use_system_expat=false " +
27+
$"skia_use_system_libjpeg_turbo=false " +
28+
$"skia_use_system_libpng=false " +
29+
$"skia_use_system_libwebp=false " +
30+
$"skia_use_system_zlib=false " +
31+
$"skia_enable_skottie=true " +
32+
$"extra_cflags=[ '-DSKIA_C_DLL', '-DHAVE_ARC4RANDOM_BUF' ] " +
33+
ADDITIONAL_GN_ARGS;
34+
1735
Task("libSkiaSharp")
1836
.IsDependentOn("git-sync-deps")
1937
.WithCriteria(IsRunningOnMacOs())
2038
.Does(() =>
2139
{
2240
if (VARIANT == "ios") {
23-
Build("iphonesimulator", "x86_64", "x64");
24-
Build("iphonesimulator", "arm64", "arm64");
25-
Build("iphoneos", "arm64", "arm64");
26-
// Build("iphoneos", "arm64", "arm64", "arm64e");
27-
28-
SafeCopy(
29-
$"libSkiaSharp/bin/{CONFIGURATION}/iphonesimulator/x86_64.xcarchive",
30-
OUTPUT_PATH.Combine($"ios/libSkiaSharp/x86_64.xcarchive"));
31-
32-
CreateFatFramework(OUTPUT_PATH.Combine("ios/libSkiaSharp"));
33-
CreateFatFramework(OUTPUT_PATH.Combine("iossimulator/libSkiaSharp"));
41+
var simX64 = Build("iphonesimulator", "x86_64", "x64");
42+
var simArm64 = Build("iphonesimulator", "arm64", "arm64");
43+
var deviceArm64 = Build("iphoneos", "arm64", "arm64");
44+
45+
// device framework (runtimes/ios): device-arm64 + legacy simulator-x86_64,
46+
// the exact arch layout the published iOS NuGet expects. The device (iphoneos)
47+
// framework is the base, so the shipped bundle advertises the device SDK.
48+
CombineFrameworks(
49+
OUTPUT_PATH.Combine("ios/libSkiaSharp.framework"),
50+
new[] { deviceArm64, simX64 });
51+
52+
// simulator framework (runtimes/iossimulator): simulator-x86_64 + simulator-arm64.
53+
CombineFrameworks(
54+
OUTPUT_PATH.Combine("iossimulator/libSkiaSharp.framework"),
55+
new[] { simX64, simArm64 });
3456
} else if (VARIANT == "maccatalyst") {
35-
Build("macosx", "x86_64", "x64");
36-
Build("macosx", "arm64", "arm64");
57+
var x64 = Build("macosx", "x86_64", "x64");
58+
var arm64 = Build("macosx", "arm64", "arm64");
3759

38-
CreateFatVersionedFramework(OUTPUT_PATH.Combine("maccatalyst/libSkiaSharp"));
60+
CombineFrameworks(
61+
OUTPUT_PATH.Combine("maccatalyst/libSkiaSharp.framework"),
62+
new[] { x64, arm64 },
63+
versioned: true);
3964
}
4065

41-
void Build(string sdk, string arch, string skiaArch, string xcodeArch = null)
66+
DirectoryPath Build(string sdk, string arch, string skiaArch, string xcodeArch = null)
4267
{
43-
if (Skip(arch)) return;
68+
if (Skip(arch)) return null;
4469

4570
xcodeArch = xcodeArch ?? arch;
4671
var isSim = sdk.EndsWith("simulator");
4772
var platform = VARIANT;
4873
if (VARIANT == "ios" && isSim)
4974
platform += "simulator";
5075

51-
GnNinja($"{platform}/{xcodeArch}", "skia modules/skottie",
52-
$"target_cpu='{skiaArch}' " +
53-
$"target_os='{VARIANT}' " +
54-
$"min_{VARIANT}_version='{GetDeploymentTarget(arch)}' " +
55-
$"ios_use_simulator={(isSim ? "true" : "false")} " +
56-
$"skia_use_harfbuzz=false " +
57-
$"skia_use_icu=false " +
58-
$"skia_use_metal=true " +
59-
$"skia_use_piex=true " +
60-
$"skia_use_system_expat=false " +
61-
$"skia_use_system_libjpeg_turbo=false " +
62-
$"skia_use_system_libpng=false " +
63-
$"skia_use_system_libwebp=false " +
64-
$"skia_use_system_zlib=false " +
65-
$"skia_enable_skottie=true " +
66-
$"extra_cflags=[ '-DSKIA_C_DLL', '-DHAVE_ARC4RANDOM_BUF' ] " +
67-
ADDITIONAL_GN_ARGS);
68-
69-
RunXCodeBuild("libSkiaSharp/libSkiaSharp.xcodeproj", "libSkiaSharp", sdk, xcodeArch, properties: new Dictionary<string, string> {
70-
{ $"{VARIANT.ToUpper()}_DEPLOYMENT_TARGET_VERSION", GetDeploymentTarget(arch) },
71-
{ $"SKIA_PLATFORM", platform },
72-
});
73-
74-
SafeCopy(
75-
$"libSkiaSharp/bin/{CONFIGURATION}/{sdk}/{xcodeArch}.xcarchive",
76-
OUTPUT_PATH.Combine($"{platform}/libSkiaSharp/{xcodeArch}.xcarchive"));
76+
// GN produces the complete single-arch lib*.framework (bundle layout, install_name,
77+
// arm64e-thinned binary and provenance Info.plist) in its out dir; we only fuse the
78+
// per-arch frameworks together afterwards.
79+
GnNinja($"{platform}/{xcodeArch}", "SkiaSharp", SkiaGnArgs(skiaArch, arch, isSim));
80+
81+
return SKIA_PATH.Combine($"out/{platform}/{xcodeArch}/libSkiaSharp.framework");
7782
}
7883
});
7984

8085
Task("libHarfBuzzSharp")
86+
.IsDependentOn("git-sync-deps")
8187
.WithCriteria(IsRunningOnMacOs())
8288
.Does(() =>
8389
{
8490
if (VARIANT == "ios") {
85-
Build("iphonesimulator", "x86_64");
86-
Build("iphonesimulator", "arm64");
87-
Build("iphoneos", "arm64");
88-
// Build("iphoneos", "arm64e");
91+
var simX64 = Build("iphonesimulator", "x86_64", "x64");
92+
var simArm64 = Build("iphonesimulator", "arm64", "arm64");
93+
var deviceArm64 = Build("iphoneos", "arm64", "arm64");
8994

90-
SafeCopy(
91-
$"libHarfBuzzSharp/bin/{CONFIGURATION}/iphonesimulator/x86_64.xcarchive",
92-
OUTPUT_PATH.Combine($"ios/libHarfBuzzSharp/x86_64.xcarchive"));
95+
CombineFrameworks(
96+
OUTPUT_PATH.Combine("ios/libHarfBuzzSharp.framework"),
97+
new[] { deviceArm64, simX64 });
9398

94-
CreateFatFramework(OUTPUT_PATH.Combine("ios/libHarfBuzzSharp"));
95-
CreateFatFramework(OUTPUT_PATH.Combine("iossimulator/libHarfBuzzSharp"));
99+
CombineFrameworks(
100+
OUTPUT_PATH.Combine("iossimulator/libHarfBuzzSharp.framework"),
101+
new[] { simX64, simArm64 });
96102
} else if (VARIANT == "maccatalyst") {
97-
Build("macosx", "x86_64");
98-
Build("macosx", "arm64");
103+
var x64 = Build("macosx", "x86_64", "x64");
104+
var arm64 = Build("macosx", "arm64", "arm64");
99105

100-
CreateFatVersionedFramework(OUTPUT_PATH.Combine("maccatalyst/libHarfBuzzSharp"));
106+
CombineFrameworks(
107+
OUTPUT_PATH.Combine("maccatalyst/libHarfBuzzSharp.framework"),
108+
new[] { x64, arm64 },
109+
versioned: true);
101110
}
102111

103-
void Build(string sdk, string arch, string xcodeArch = null)
112+
DirectoryPath Build(string sdk, string arch, string skiaArch)
104113
{
105-
if (Skip(arch)) return;
114+
if (Skip(arch)) return null;
106115

107-
xcodeArch = xcodeArch ?? arch;
108116
var isSim = sdk.EndsWith("simulator");
109117
var platform = VARIANT;
110118
if (VARIANT == "ios" && isSim)
111119
platform += "simulator";
112120

113-
RunXCodeBuild("libHarfBuzzSharp/libHarfBuzzSharp.xcodeproj", "libHarfBuzzSharp", sdk, xcodeArch, properties: new Dictionary<string, string> {
114-
{ $"{VARIANT.ToUpper()}_DEPLOYMENT_TARGET_VERSION", GetDeploymentTarget(arch) },
115-
});
121+
// Reuse the same out dir + args as libSkiaSharp (identical args => no re-gen);
122+
// only the ninja target differs. The HarfBuzzSharp GN target is self-contained.
123+
GnNinja($"{platform}/{arch}", "HarfBuzzSharp", SkiaGnArgs(skiaArch, arch, isSim));
116124

117-
SafeCopy(
118-
$"libHarfBuzzSharp/bin/{CONFIGURATION}/{sdk}/{xcodeArch}.xcarchive",
119-
OUTPUT_PATH.Combine($"{platform}/libHarfBuzzSharp/{xcodeArch}.xcarchive"));
125+
return SKIA_PATH.Combine($"out/{platform}/{arch}/libHarfBuzzSharp.framework");
120126
}
121127
});
122128

0 commit comments

Comments
 (0)