diff --git a/packages/skia/CONTRIBUTING.md b/packages/skia/CONTRIBUTING.md index b7efb5ee90..dbb176150c 100644 --- a/packages/skia/CONTRIBUTING.md +++ b/packages/skia/CONTRIBUTING.md @@ -129,6 +129,43 @@ The npm packages this library consumes (`react-native-skia-android`, `react-nati Back in this repo, bump the prebuilt binary versions in `packages/skia/package.json` (`react-native-skia-android` and `react-native-skia-apple-*`) to the versions you just published, run `yarn`, and re-run `pod install` in the example app so it consumes the released binaries. Drop the throwaway `node_modules` and `libs/ios/.version` edits from step 3. +### Swift Package Manager (preview) + +CocoaPods stays the default. `Package.swift` is additive: SwiftPM ignores the +podspec, and CocoaPods ignores `Package.swift`. + +SwiftPM support requires **React Native 0.87 or newer** — earlier releases ship +no `scripts/spm`. `apps/example` is on an older version, so it cannot exercise +this path. + +Autolinking references the library through a symlink at +`/ios/build/generated/autolinking/libs/ReactNativeSkia`, and SwiftPM +resolves the manifest's relative paths against that symlink rather than against +`packages/skia`. The two React Native package paths are therefore identical for +every standard app. The target name is pinned in `react-native.config.js`; +without it a future React Native release would derive it from the podspec +instead and change the header import prefix. + +Skia's Apple sources still gate on `RCT_NEW_ARCH_ENABLED` and +`RCT_REMOVE_LEGACY_ARCH`. CocoaPods forces both project-wide; the SwiftPM path +defines neither, so `Package.swift` defines them itself. + +#### Binaries + +The manifest links the `react-native-skia-apple-ios` npm package, the same one +the CocoaPods build uses, so no network is needed once dependencies are +installed. It is resolved by path, from either a sibling in `node_modules` or +this monorepo's root, and the manifest fails with an explanatory message when +neither exists — which is what an `--omit=optional` install looks like. + +Fetching the binaries from a released Swift package instead is future work; it +becomes useful only once the binary npm packages are no longer dependencies. See +[wcandillon/react-native-skia-binaries](https://github.com/wcandillon/react-native-skia-binaries). + +After changing which binaries a checkout uses, delete +`ios/.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` +— a stale pin silently keeps the previous source. + ### Publishing - Run the commands in the [Building](#building) section diff --git a/packages/skia/Package.swift b/packages/skia/Package.swift new file mode 100644 index 0000000000..b6c6b9a9cc --- /dev/null +++ b/packages/skia/Package.swift @@ -0,0 +1,117 @@ +// swift-tools-version: 6.0 +// +// SwiftPM manifest for @shopify/react-native-skia. iOS, Ganesh. +// Additive: the CocoaPods podspec remains the supported default. +// +// React Native 0.87+ references a library that ships its own Package.swift +// through a symlink at /ios/build/generated/autolinking/libs/, and +// resolves the relative paths below against that symlink. They are the same +// for every standard app, because the symlink location is. + +import Foundation +import PackageDescription + +// Skia's xcframeworks are ~200MB and are not carried in this package. They come +// from the react-native-skia-apple-ios npm package, which @shopify/react-native-skia +// depends on, so a checkout that has installed its dependencies builds offline. +// Xcode passes the symlink as the package directory, hence resolvingSymlinks. +let packageRoot = URL(fileURLWithPath: Context.packageDirectory) + .resolvingSymlinksInPath().path + +let binariesPath = [ + "../../react-native-skia-apple-ios", // consumer: sibling in node_modules + "../../node_modules/react-native-skia-apple-ios", // this monorepo +] +.map { "\(packageRoot)/\($0)" } +.first { FileManager.default.fileExists(atPath: "\($0)/Package.swift") } + +guard let binariesPath else { + // Reached when the package was skipped at install time (`--omit=optional`, + // or a non-Apple `os` filter), which SwiftPM would otherwise report as an + // unresolvable dependency path. + fatalError( + """ + react-native-skia-apple-ios was not found next to @shopify/react-native-skia. \ + It ships the prebuilt Skia binaries this package links against. Reinstall \ + dependencies without --omit=optional, on macOS. + """) +} + +let package = Package( + name: "ReactNativeSkia", + platforms: [.iOS(.v15)], + products: [ + // Autolinking looks this name up verbatim; react-native.config.js pins it. + .library(name: "ReactNativeSkia", targets: ["ReactNativeSkia"]) + ], + dependencies: [ + .package(name: "React-GeneratedCode", path: "../../../ios"), + .package(name: "ReactNative", path: "../../../../xcframeworks"), + .package(path: binariesPath), + ], + targets: [ + .target( + name: "ReactNativeSkia", + dependencies: [ + .product(name: "ReactHeaders", package: "ReactNative"), + .product(name: "ReactNativeHeaders", package: "ReactNative"), + .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"), + .product(name: "ReactAppHeaders", package: "React-GeneratedCode"), + .product(name: "react-native-skia-apple-ios", package: "react-native-skia-apple-ios"), + ], + // apple/ and cpp/ have no common ancestor below the package root, and + // .headerSearchPath cannot escape the target path. + path: ".", + exclude: [ + "cpp/rnskia/RNDawnWindowContext.cpp", // Graphite only + "cpp/rnskia/RNDawnInterop.cpp", // Graphite only + ], + // Explicit, so SwiftPM never walks node_modules, lib, android, or the + // headers-only cpp/skia — which carries x86 AVX skcms sources that + // cannot build for arm64. + sources: [ + "apple", + "cpp/api", + "cpp/jsi", + "cpp/rnskia", + "cpp/utils", + // libskottie.a needs skjson and Apple builds no libskjson archive. + // CocoaPods compiles this via its cpp/**/*.cpp glob. + "cpp/skia/modules/jsonreader/SkJSONReader.cpp", + ], + cxxSettings: [ + // Replaces the podspec's recursive cpp/** glob, which expands to 90. + .headerSearchPath("cpp"), // "api/…", "jsi/…", "utils/…" + .headerSearchPath("cpp/skia"), // "include/core/…", "modules/…", "src/…" + .headerSearchPath("cpp/rnskia"), // apple/ uses bare "RNSkView.h" + .headerSearchPath("cpp/utils"), // apple/ uses bare "RNSkLog.h" + + // CocoaPods forces both project-wide; the SwiftPM path defines neither. + // Skia's Apple sources still gate on them: without them RNSkiaModule's + // legacy branch fails to compile and -getTurboModule: is dropped, so + // the JSI bindings never install. + .define("RCT_NEW_ARCH_ENABLED", to: "1"), + .define("RCT_REMOVE_LEGACY_ARCH", to: "1"), + + .define("SK_METAL", to: "1"), + .define("SK_GANESH", to: "1"), + .define("SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API", to: "1"), + .define("SK_DISABLE_LEGACY_SHAPER_FACTORY", to: "1"), + + // React's prebuilt C++ ABI is NDEBUG-gated, and Skia derives + // SK_RELEASE from it. Omitting either breaks the Release link. + .define("DEBUG", .when(configuration: .debug)), + .define("NDEBUG", .when(configuration: .release)), + ], + linkerSettings: [ + .linkedFramework("MetalKit"), + .linkedFramework("AVFoundation"), + .linkedFramework("AVKit"), + .linkedFramework("CoreMedia"), + ] + ) + ], + // React Native's headers need C++20, and so does Skia m152: SkMathPriv.h + // calls std::countl_zero, std::countr_zero and std::popcount. + cxxLanguageStandard: .cxx20 +) diff --git a/packages/skia/include/ReactNativeSkia.h b/packages/skia/include/ReactNativeSkia.h new file mode 100644 index 0000000000..7a6c49d136 --- /dev/null +++ b/packages/skia/include/ReactNativeSkia.h @@ -0,0 +1,6 @@ +#pragma once + +// Umbrella header for the SwiftPM target's publicHeadersPath. Nothing outside +// this package includes Skia or RNSkia headers — React Native resolves the +// Fabric component and TurboModule by class name — so the public surface is +// deliberately empty. diff --git a/packages/skia/package.json b/packages/skia/package.json index 999c56c467..605aa28a00 100644 --- a/packages/skia/package.json +++ b/packages/skia/package.json @@ -32,6 +32,9 @@ "cpp/**/*.{h,cpp}", "apple/**", "react-native-skia.podspec", + "Package.swift", + "include/**", + "react-native.config.js", "dist/**", "libs/.graphite" ], diff --git a/packages/skia/react-native.config.js b/packages/skia/react-native.config.js new file mode 100644 index 0000000000..9892e6fecf --- /dev/null +++ b/packages/skia/react-native.config.js @@ -0,0 +1,5 @@ +// Pins the SwiftPM target name. Without it the name is derived — today from the +// npm name (ReactNativeSkia), but an in-flight React Native change derives it +// from the podspec instead (react-native-skia). The name is also the header +// import prefix, so it must not move. +module.exports = {spm: {name: 'ReactNativeSkia'}};