From d6e449e88c3130c7df9871a30cefe81b12adfc0e Mon Sep 17 00:00:00 2001 From: Christian Falch <875252+chrfalch@users.noreply.github.com> Date: Wed, 2 Feb 2022 11:28:14 +0100 Subject: [PATCH] Feature/skia m99 (#165) * Updated to Skia branch chrome/m99 * Updated API to Skia Chrome/M99 * Updated podspec to link against xcframeworks * Updated build system to build for Apple Silicon M1 Simulator - Added use of xcframeworks to be able to package archs and platforms correctly - Fixed build in Skia iOS to correctly build for arc and platform - Updated configuration with more flexible setup and typed structs. - Removed android aar build script * Moved building frameworks to the build-skia-ios step * Updated to run build ios frameworks * Removed arm64 from excluded archs in release build for simulator * - Added clean step for Skia - Added README update for upgrading Skia * Changed build scripts to be manually triggered - Releasing NPM packages are now manually triggered - Building SKIA is now manually triggered. * Fixed wrong path in clean * Fixed lint issue in Skia.ts * Commented out arm (32 bit) arc for iOS Skia build Co-authored-by: William Candillon --- .github/workflows/build-npm.yml | 5 +- .github/workflows/build-skia.yml | 21 +-- README.md | 40 +++-- example/ios/RNSkia.xcodeproj/project.pbxproj | 4 +- externals/skia | 2 +- package.json | 15 +- package/cpp/api/JsiSkCanvas.h | 2 +- package/cpp/api/JsiSkDataFactory.h | 14 +- package/cpp/api/JsiSkRuntimeEffect.h | 6 +- package/package.json | 10 +- package/react-native-skia.podspec | 8 +- package/src/skia/Skia.ts | 1 - scripts/build-android.ts | 69 ------- scripts/build-npm-package.ts | 25 ++- scripts/build-skia-android.ts | 17 ++ scripts/build-skia-ios-fatlibs.ts | 36 ---- scripts/build-skia-ios-framework.ts | 60 +++++++ scripts/build-skia-ios.ts | 17 ++ scripts/build-skia.ts | 178 ++++++++++++------- scripts/skia-configuration.ts | 108 +++++++++-- scripts/utils.ts | 6 +- scripts/workflow-copy-libs.ts | 10 +- 22 files changed, 395 insertions(+), 259 deletions(-) delete mode 100644 scripts/build-android.ts create mode 100644 scripts/build-skia-android.ts delete mode 100644 scripts/build-skia-ios-fatlibs.ts create mode 100644 scripts/build-skia-ios-framework.ts create mode 100644 scripts/build-skia-ios.ts diff --git a/.github/workflows/build-npm.yml b/.github/workflows/build-npm.yml index ca6baeedd4..c334f7a72e 100644 --- a/.github/workflows/build-npm.yml +++ b/.github/workflows/build-npm.yml @@ -1,8 +1,5 @@ name: Build NPM Package -on: - push: - branches: - - main +on: workflow_dispatch jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/build-skia.yml b/.github/workflows/build-skia.yml index 3d7e7b734b..3b426952bc 100644 --- a/.github/workflows/build-skia.yml +++ b/.github/workflows/build-skia.yml @@ -1,10 +1,5 @@ name: Build SKIA -on: - push: - branches: - - main - paths: - - externals/** +on: workflow_dispatch jobs: build: #runs-on: ubuntu-latest @@ -52,9 +47,9 @@ jobs: ANDROID_NDK: ${{ steps.setup-ndk.outputs.ndk-path }} run: yarn build-skia - - name: Build Skia iOS Fat libs + - name: Build Skia iOS xcframeworks working-directory: ${{ env.WORKING_DIRECTORY }}/ - run: yarn build-skia-ios-fatlibs + run: yarn build-skia-ios-frameworks - name: Upload artifacts - Android arm uses: actions/upload-artifact@v2 @@ -92,11 +87,11 @@ jobs: ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libskshaper.a ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libsvg.a - - name: Upload artifacts - iOS Fat Libs + - name: Upload artifacts - iOS xcframeworks uses: actions/upload-artifact@v2 with: - name: skia-ios-fat-libs + name: skia-ios-xcframeworks path: | - ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskia.a - ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskshaper.a - ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libsvg.a + ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskia.xcframework + ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskshaper.xcframework + ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libsvg.xcframework diff --git a/README.md b/README.md index b3ce43b4b8..c8ef2e8263 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Checkout the full documentation [here](https://shopify.github.io/react-native-sk React Native Skia brings the Skia Graphics Library to React Native. Skia serves as the graphics engine for Google Chrome and Chrome OS, Android, Flutter, Mozilla Firefox and Firefox OS, and many other products. -*This is an alpha release. Use with caution.* +_This is an alpha release. Use with caution._ ## Installation @@ -30,7 +30,9 @@ npm install https://github.com/Shopify/react-native-skia/releases/download/v0.1. Run `pod install` on the `ios/` directory. -You will need to disable Bitcode in order to create a release build: `Build Settings > Build Options > Enable Bitcode -> Release -> No`. In Expo managed apps, set `ios.bitcode` to `false` in `app.json`. +| NOTE: Device builds now includes Bitcode generation on iOS - so there is no longer necessary to build with bitcode disabled for release builds. + +| You can re-enable Bitcode if it was previously disabled: `Build Settings > Build Options > Enable Bitcode -> Release -> Yes`. In Expo managed apps, set `ios.bitcode` to `true` in `app.json`. ### Android @@ -53,7 +55,6 @@ For error **_CMake 'X.X.X' was not found in SDK, PATH, or by cmake.dir property. open _Tools > SDK Manager_, switch to the _SDK Tools_ tab. Find `CMake` and click _Show Package Details_ and download compatiable version **'X.X.X'**, and apply to install. - ### Playground We have an example project you can play with [here](https://github.com/Shopify/react-native-skia/tree/main/example). @@ -68,7 +69,6 @@ To run the example project on iOS, you will need to run `pod install` and on And ## Hello World - React Native Skia has two APIs: a declarative API available as a React Native Renderer and an imperative API backed by JSI. The recommended way to use this library is via the declarative API. Library developers may take advantage of the imperative API to provide custom features. @@ -78,7 +78,7 @@ Library developers may take advantage of the imperative API to provide custom fe ### Example ```tsx twoslash -import {Canvas, Circle, Group} from "@shopify/react-native-skia"; +import { Canvas, Circle, Group } from "@shopify/react-native-skia"; export const HelloWorld = () => { const width = 256; @@ -89,12 +89,7 @@ export const HelloWorld = () => { - + ); @@ -106,7 +101,12 @@ export const HelloWorld = () => { ### Example ```tsx twoslash -import {Skia, BlendMode, SkiaView, useDrawCallback} from "@shopify/react-native-skia"; +import { + Skia, + BlendMode, + SkiaView, + useDrawCallback, +} from "@shopify/react-native-skia"; const paint = Skia.Paint(); paint.setAntiAlias(true); @@ -128,11 +128,9 @@ export const HelloWorld = () => { // Yellow Circle const yellow = paint.copy(); yellow.setColor(Skia.Color("yellow")); - canvas.drawCircle(width/2, height - r, r, yellow); + canvas.drawCircle(width / 2, height - r, r, yellow); }); - return ( - - ); + return ; }; ``` @@ -170,6 +168,16 @@ And then the _SDK Location_ section. It will show you the NDK path, or the optio - Copy Skia headers `yarn copy-skia-headers` - Run pod install in the example project +### Upgrading + +If a new version of Skia is included in an upgrade of this library, you need to perform a few extra steps before continuing: + +1. Update submodules: `git submodule update --recursive` +2. Copy Skia Headers: `yarn copy-skia-headers` +3. Clean Skia: `yarn clean-skia` +4. Build Skia: `yarn build-skia` +5. Run pod install in the example project + ### Publishing - Run the commands in the `Building` section diff --git a/example/ios/RNSkia.xcodeproj/project.pbxproj b/example/ios/RNSkia.xcodeproj/project.pbxproj index 251dc4134b..817cd91f51 100644 --- a/example/ios/RNSkia.xcodeproj/project.pbxproj +++ b/example/ios/RNSkia.xcodeproj/project.pbxproj @@ -564,7 +564,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 "; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -630,7 +630,7 @@ COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 "; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/externals/skia b/externals/skia index 0b898c4b8e..51988d0bc1 160000 --- a/externals/skia +++ b/externals/skia @@ -1 +1 @@ -Subproject commit 0b898c4b8e2a5291d378a99701a533eaf1d4d301 +Subproject commit 51988d0bc11e9024254789aad35b5d86bca2779c diff --git a/package.json b/package.json index d5066a0129..6b3f385f56 100644 --- a/package.json +++ b/package.json @@ -15,21 +15,14 @@ "typescript": "^4.4.4" }, "scripts": { - "build-skia-ios-arm": "ts-node ./scripts/build-skia.ts ios arm", - "build-skia-ios-arm64": "ts-node ./scripts/build-skia.ts ios arm64", - "build-skia-ios-x64": "ts-node ./scripts/build-skia.ts ios x64", - "build-skia-android-arm": "ts-node ./scripts/build-skia.ts android arm", - "build-skia-android-arm64": "ts-node ./scripts/build-skia.ts android arm64", - "build-skia-android-x86": "ts-node ./scripts/build-skia.ts android x86", - "build-skia-android-x64": "ts-node ./scripts/build-skia.ts android x64", - "build-skia-ios-fatlibs": "ts-node ./scripts/build-skia-ios-fatlibs.ts", - "build-skia-ios": "yarn build-skia-ios-x64 && yarn build-skia-ios-arm64 && yarn build-skia-ios-arm && yarn build-skia-ios-fatlibs", - "build-skia-android": "yarn build-skia-android-x64 && yarn build-skia-android-arm64 && yarn build-skia-android-arm && yarn build-skia-android-x86", + "build-skia-ios-framework": "ts-node ./scripts/build-skia-ios-framework.ts", + "build-skia-ios": "ts-node ./scripts/build-skia-ios.ts && yarn build-skia-ios-framework", + "build-skia-android": "ts-node ./scripts/build-skia-android.ts", "build-skia": "yarn build-skia-ios && yarn build-skia-android", + "clean-skia": "yarn rimraf ./package/libs && yarn rimraf ./externals/skia/out", "copy-skia-include-headers": "yarn rimraf ./package/cpp/skia/include/ && cp -a ./externals/skia/include/. ./package/cpp/skia/include", "copy-skia-module-headers": "yarn rimraf ./package/cpp/skia/modules/ && mkdir -p ./package/cpp/skia/modules/svg/include && mkdir -p ./package/cpp/skia/modules/skresources/include && cp -a ./externals/skia/modules/svg/include/. ./package/cpp/skia/modules/svg/include && cp -a ./externals/skia/modules/skresources/include/. ./package/cpp/skia/modules/skresources/include", "copy-skia-headers": "yarn copy-skia-module-headers && yarn copy-skia-include-headers", - "build-android-aar": "yarn ts-node ./scripts/build-android.ts", "build-npm": "yarn ts-node ./scripts/build-npm-package.ts", "get-filename-npm": "yarn ts-node ./scripts/get-npm-filename.ts", "get-version-npm": "yarn ts-node ./scripts/get-npm-version.ts", diff --git a/package/cpp/api/JsiSkCanvas.h b/package/cpp/api/JsiSkCanvas.h index da716d40bf..d5309725e1 100644 --- a/package/cpp/api/JsiSkCanvas.h +++ b/package/cpp/api/JsiSkCanvas.h @@ -281,7 +281,7 @@ class JsiSkCanvas : public JsiSkHostObject { _canvas->drawPatch(cubics.data(), colors.data(), texs.data(), blendMode, *paint); } else { - _canvas->drawPatch(cubics.data(), colors.data(), texs.data(), *paint); + _canvas->drawPatch(cubics.data(), colors.data(), texs.data(), SkBlendMode::kClear, *paint); } return jsi::Value::undefined(); } diff --git a/package/cpp/api/JsiSkDataFactory.h b/package/cpp/api/JsiSkDataFactory.h index 63232c8b3a..d1d0304c09 100644 --- a/package/cpp/api/JsiSkDataFactory.h +++ b/package/cpp/api/JsiSkDataFactory.h @@ -57,12 +57,20 @@ namespace RNSkia { // Calculate length size_t len; - SkBase64::Decode(&base64.utf8(runtime).c_str()[0], size, nullptr, &len); + auto err = SkBase64::Decode(&base64.utf8(runtime).c_str()[0], size, nullptr, &len); + if(err != SkBase64::Error::kNoError) { + jsi::detail::throwJSError(runtime, "Error decoding base64 string"); + return jsi::Value::undefined(); + } // Create data object and decode auto data = SkData::MakeUninitialized(len); - SkBase64::Decode(&base64.utf8(runtime).c_str()[0], size, data->writable_data(), &len); - + err = SkBase64::Decode(&base64.utf8(runtime).c_str()[0], size, data->writable_data(), &len); + if(err != SkBase64::Error::kNoError) { + jsi::detail::throwJSError(runtime, "Error decoding base64 string"); + return jsi::Value::undefined(); + } + return jsi::Object::createFromHostObject(runtime, std::make_shared( getContext(), data)); diff --git a/package/cpp/api/JsiSkRuntimeEffect.h b/package/cpp/api/JsiSkRuntimeEffect.h index 3b744c152f..32d35895f2 100644 --- a/package/cpp/api/JsiSkRuntimeEffect.h +++ b/package/cpp/api/JsiSkRuntimeEffect.h @@ -72,7 +72,7 @@ namespace RNSkia JSI_HOST_FUNCTION(getUniformCount) { - return static_cast(getObject()->uniforms().count()); + return static_cast(getObject()->uniforms().size()); } JSI_HOST_FUNCTION(getUniformFloatCount) @@ -82,14 +82,14 @@ namespace RNSkia JSI_HOST_FUNCTION(getUniformName) { - auto i = arguments[0].asNumber(); + auto i = static_cast(arguments[0].asNumber()); auto it = getObject()->uniforms().begin() + i; return jsi::String::createFromAscii(runtime, it->name.c_str()); } JSI_HOST_FUNCTION(getUniform) { - auto i = arguments[0].asNumber(); + auto i = static_cast(arguments[0].asNumber()); auto it = getObject()->uniforms().begin() + i; auto result = jsi::Object(runtime); RuntimeEffectUniform su = fromUniform(*it); diff --git a/package/package.json b/package/package.json index 7bf923daaa..037a9bdd47 100644 --- a/package/package.json +++ b/package/package.json @@ -4,7 +4,7 @@ "access": "public" }, "title": "React Native Skia", - "version": "0.1.37", + "version": "0.1.100", "description": "Skia View for React Native", "main": "index.ts", "files": [ @@ -19,9 +19,9 @@ "index.js", "cpp/**/*.{h,cpp}", "ios", - "libs/ios/libskia.a", - "libs/ios/libskshaper.a", - "libs/ios/libsvg.a", + "libs/ios/libskia.xcframework", + "libs/ios/libskshaper.xcframework", + "libs/ios/libsvg.xcframework", "react-native-skia.podspec", "scripts/install-npm.js" ], @@ -72,4 +72,4 @@ "dependencies": { "react-reconciler": "^0.26.2" } -} +} \ No newline at end of file diff --git a/package/react-native-skia.podspec b/package/react-native-skia.podspec index 123cec5fcd..4f0cf73e67 100644 --- a/package/react-native-skia.podspec +++ b/package/react-native-skia.podspec @@ -27,10 +27,10 @@ Pod::Spec.new do |s| s.frameworks = 'GLKit', 'MetalKit' - s.ios.vendored_libraries = [ - 'libs/ios/libskia.a', - 'libs/ios/libsvg.a', - 'libs/ios/libskshaper.a' + s.ios.vendored_frameworks = [ + 'libs/ios/libskia.xcframework', + 'libs/ios/libsvg.xcframework', + 'libs/ios/libskshaper.xcframework' ] # All iOS cpp/h files diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index 88aaa6d5fc..6c2e5336d1 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -21,7 +21,6 @@ import type { FontMgrFactory } from "./FontMgr/FontMgrFactory"; import type { SurfaceFactory } from "./Surface"; import "./NativeSetup"; - /** * Declares the interface for the native Skia API */ diff --git a/scripts/build-android.ts b/scripts/build-android.ts deleted file mode 100644 index ddecc2d77a..0000000000 --- a/scripts/build-android.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { exit } from "process"; -import { configurations } from "./skia-configuration"; -import { executeCmdSync, checkFileExists } from "./utils"; - -/** - * This build script builds the debug/release aar files for the android - * version of the library. - * - * The prerequisites are: - * - * - NDK installed - * - Prebuilt Skia binaries available - * - Skia Headers copied - * - * The script does not take or use any parameters. - * - * NOTE: This script is not currently in use since we are - * distributing the library with sources in the first versions. - * - */ -console.log("Testing to see if everything is set up correctly..."); -console.log(""); -// Test for existence of Android SDK -if (!process.env.ANDROID_NDK) { - console.log("ANDROID_NDK not set."); - exit(1); -} else { - console.log("☑ ANDROID_NDK"); -} - -// Test for prebuilt Skia binaries -configurations.android.outputMapping!.forEach((abi) => { - ["libskia.a", "libskshaper.a", "libsvg.a"].forEach((lib) => { - checkFileExists( - `./package/libs/android/${abi}/${lib}`, - `Skia Android ${abi}/${lib}`, - "Have you built the Skia Android binaries? Run yarn run build-skia (it takes a while)" - ); - }); -}); - -// Test / make sure headers are copied -checkFileExists( - "./package/cpp/skia/readme.txt", - "Skia Headers Copied", - "Have you copied the Skia headers? Run yarn run copy-skia-headers to copy the headers." -); - -console.log(""); - -console.log("Building Android release/debug aar libraries..."); -const currentDir = process.cwd(); - -console.log("Entering example/android"); -process.chdir("./example/android"); - -const buildVariant = (variant: "Debug" | "Release") => { - console.log(`Building ${variant}...`); - executeCmdSync(`./gradlew :shopify_react-native-skia:assemble${variant}`); - console.log(`Done building ${variant}.`); -}; - -console.log("Cleaning build folders ..."); -executeCmdSync("./gradlew :shopify_react-native-skia:clean"); - -buildVariant("Debug"); -buildVariant("Release"); - -process.chdir(currentDir); diff --git a/scripts/build-npm-package.ts b/scripts/build-npm-package.ts index d689d3d0e0..0436359b31 100644 --- a/scripts/build-npm-package.ts +++ b/scripts/build-npm-package.ts @@ -38,18 +38,27 @@ if (process.env.GITHUB_RUN_NUMBER === undefined) { } // Check that Android Skia libs are built -configurations.android.outputMapping!.forEach((cpu) => - ["libskia.a", "libskshaper.a", "libsvg.a"].forEach((lib) => { +Object.keys(configurations.android.targets).forEach((targetKey) => { + const target = configurations.android.targets[targetKey]; + configurations.android.outputNames.forEach((name) => { + const path = `./package/libs/android/${ + target.output ?? target.cpu + }/${name}`; + checkFileExists( - `./package/libs/android/${cpu}/${lib}`, - `Skia Android ${cpu}/${lib}`, + path, + `Skia Android ${path}`, "Have you built the Skia Android binaries? Run yarn run build." ); - }) -); + }); +}); -// Check that iOS Skia libs are built -["libskia.a", "libskshaper.a", "libsvg.a"].forEach((lib) => { +// Check that iOS Skia frameworks are built +[ + "libskia.xcframework", + "libskshaper.xcframework", + "libsvg.xcframework", +].forEach((lib) => { checkFileExists( `./package/libs/ios/${lib}`, `Skia iOS ${lib}`, diff --git a/scripts/build-skia-android.ts b/scripts/build-skia-android.ts new file mode 100644 index 0000000000..f3a78e05b6 --- /dev/null +++ b/scripts/build-skia-android.ts @@ -0,0 +1,17 @@ +import { configurations } from "./skia-configuration"; +import { executeCmd } from "./utils"; + +const configuration = configurations.android; + +console.log("Building skia for Android..."); +let command = ""; + +Object.keys(configuration.targets).forEach((targetKey) => { + command += + (command !== "" ? " && " : "") + + `yarn ts-node ./scripts/build-skia.ts android ${targetKey}`; +}); + +executeCmd(command, "Android", () => { + console.log(`Done building skia for Android.`); +}); diff --git a/scripts/build-skia-ios-fatlibs.ts b/scripts/build-skia-ios-fatlibs.ts deleted file mode 100644 index 304d4e569a..0000000000 --- a/scripts/build-skia-ios-fatlibs.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable max-len */ -import { configurations } from "./skia-configuration"; -import { executeCmdSync, checkFileExists } from "./utils"; - -/** - * This build script takes the prebuilt Skia Binaries and creates - * iOS Fat Libraries (archives with all archs inside of one file). - * - * Requirements: Requires and tests that all Skia Binaries for the iOS - * archs are built and available in the libs folder. - * - * This build script is run after the Skia Binaries are built. - */ - -console.log("Building iOS Fat Libraries from Skia Binaries"); -console.log(""); - -console.log("Checking prerequisites..."); - -// Check that iOS Skia libs are built -configurations.ios.cpus.forEach((cpu) => - configurations.ios.outputNames.forEach((lib) => { - checkFileExists( - `./package/libs/ios/${cpu}/${lib}`, - `Skia iOS ${cpu}/${lib}`, - "Have you built the Skia iOS binaries? Run yarn run build." - ); - }) -); - -configurations.ios.outputNames.forEach((lib) => { - const paths = configurations.ios.cpus.map((cpu) => `package/libs/ios/${cpu}/${lib}`).join(' '); - executeCmdSync( - `lipo -create ${paths} -output package/libs/ios/${lib}` - ); -}); diff --git a/scripts/build-skia-ios-framework.ts b/scripts/build-skia-ios-framework.ts new file mode 100644 index 0000000000..ee6754ac9f --- /dev/null +++ b/scripts/build-skia-ios-framework.ts @@ -0,0 +1,60 @@ +/* eslint-disable max-len */ +import { configurations } from "./skia-configuration"; +import { executeCmdSync, checkFileExists } from "./utils"; + +/** + * This build script takes the prebuilt Skia Binaries and creates + * iOS Fat Libraries (archives with all archs inside of one file). + * + * Requirements: Requires and tests that all Skia Binaries for the iOS + * archs are built and available in the libs folder. + * + * This build script is run after the Skia Binaries are built. + */ + +console.log("Building iOS Fat Libraries from Skia Binaries"); +console.log(""); + +console.log("Checking prerequisites..."); + +// Check deps +Object.keys(configurations.ios.targets).forEach((targetKey) => { + configurations.ios.outputNames.forEach((out) => { + checkFileExists( + `package/libs/ios/${targetKey}/${out}`, + `package/libs/ios/${targetKey}/${out}`, + `package/libs/ios/${targetKey}/${out} not found` + ); + }); +}); + +console.log(""); +console.log("Prerequisites met. Starting build."); +console.log(""); + +console.log("Building fat binary for iphone simulator"); +configurations.ios.outputNames.forEach((out) => { + console.log(`Building fat binary for simulator for file ${out}`); + executeCmdSync( + `lipo -create package/libs/ios/x64/${out} package/libs/ios/arm64-iphonesimulator/${out} -output package/libs/ios/${ + out.split(".")[0] + }.a` + ); +}); + +console.log(""); +console.log("Building xcframeworks..."); + +configurations.ios.outputNames.forEach((out) => { + const libName = out.split(".")[0]; + console.log(`Building ${libName}.xcframework`); + executeCmdSync(`rm -rf ./package/libs/ios/${libName}.xcframework`); + executeCmdSync( + "xcodebuild -create-xcframework " + + `-library ./package/libs/ios/${libName}.a ` + + `-library ./package/libs/ios/arm64-iphoneos/${libName}.a ` + + ` -output ./package/libs/ios/${libName}.xcframework ` + ); +}); + +console.log("Frameworks successfully built."); diff --git a/scripts/build-skia-ios.ts b/scripts/build-skia-ios.ts new file mode 100644 index 0000000000..20bab85771 --- /dev/null +++ b/scripts/build-skia-ios.ts @@ -0,0 +1,17 @@ +import { configurations } from "./skia-configuration"; +import { executeCmd } from "./utils"; + +const configuration = configurations.ios; + +console.log("Building skia for iOS..."); +let command = ""; + +Object.keys(configuration.targets).forEach((targetKey) => { + command += + (command !== "" ? " && " : "") + + `yarn ts-node ./scripts/build-skia.ts ios ${targetKey}`; +}); + +executeCmd(command, "iOS", () => { + console.log(`Done building skia for iOS.`); +}); diff --git a/scripts/build-skia.ts b/scripts/build-skia.ts index fb8a3dd22f..6a74a00f7f 100644 --- a/scripts/build-skia.ts +++ b/scripts/build-skia.ts @@ -1,6 +1,12 @@ import { executeCmd, executeCmdSync } from "./utils"; import { exit } from "process"; -import { commonArgs, configurations, PlatformName } from "./skia-configuration"; +import { + commonArgs, + configurations, + iPhoneosSdk, + iPhoneSimulatorSdk, + PlatformName, +} from "./skia-configuration"; const fs = require("fs"); const typedKeys = (obj: T) => Object.keys(obj) as (keyof T)[]; @@ -31,59 +37,99 @@ if (!process.env.ANDROID_NDK) { console.log("☑ ANDROID_NDK"); } +// Test for existence of iOS SDK +if (!iPhoneosSdk) { + console.log("iPhoneOS SDK not set."); + exit(1); +} else { + console.log("☑ iPhoneOS SDK"); +} + +if (!iPhoneSimulatorSdk) { + console.log("iPhoneSimulatorOS SDK not set."); + exit(1); +} else { + console.log("☑ iPhoneSimulatorOS SDK"); +} +console.log(""); +console.log("Requirements met. Starting build."); +console.log(""); + if (process.argv.length !== 4) { - console.log("Missing platform/cpu arguments"); - console.log("Available platforms:"); + console.log("Missing platform/target arguments"); + console.log("Available platforms/targets:"); console.log(""); typedKeys(configurations).forEach((platform) => { console.log(platform); const config = configurations[platform]; - config.cpus.forEach((cpu) => console.log(" " + cpu)); + Object.keys(config.targets).forEach((target) => console.log(" " + target)); }); exit(1); } const currentDir = process.cwd(); const SkiaDir = "./externals/skia"; -const SelectedPlatform = process.argv[2] ?? ""; -const SelectedCpu = process.argv[3] ?? ""; +const SelectedPlatform = (process.argv[2] as PlatformName) ?? ""; +const SelectedTarget = process.argv[3] ?? ""; if (SkiaDir === undefined) { throw new Error("No Skia root directory specified."); } -const getOutDir = (platform: PlatformName, cpu: string) => { - return `out/${platform}/${cpu}`; +const getOutDir = (platform: PlatformName, targetName: string) => { + return `out/${platform}/${targetName}`; }; -const configurePlatform = (platform: PlatformName, cpu: string) => { - console.log(`Configuring platform "${platform}" for cpu "${cpu}"`); +const configurePlatform = (platform: PlatformName, targetName: string) => { + console.log(`Configuring platform "${platform}" for target "${targetName}"`); console.log("Current directory", process.cwd()); const configuration = configurations[platform]; if (configuration) { + const target = configuration.targets[targetName]; + if (!target) { + console.log(`Target ${targetName} not found for platform ${platform}`); + exit(1); + } + const commandline = `PATH=../depot_tools/:$PATH gn gen ${getOutDir( platform, - cpu + targetName )}`; const args = configuration.args.reduce( - (a, cur) => (a += `${cur[0]}=${cur[1]} `), + (a, cur) => (a += `${cur[0]}=${cur[1]} \n`), "" ); const common = commonArgs.reduce( - (a, cur) => (a += `${cur[0]}=${cur[1]} `), + (a, cur) => (a += `${cur[0]}=${cur[1]} \n`), "" ); - const command = `${commandline} --args='target_os="${platform}" target_cpu="${cpu}" ${common} ${args}'`; + const targetArgs = + target.args?.reduce((a, cur) => (a += `${cur[0]}=${cur[1]} \n`), "") || + ""; + + const options = + configuration.options?.reduce( + (a, cur) => (a += `--${cur[0]}=${cur[1]} `), + "" + ) || ""; + + const targetOptions = + target.options?.reduce((a, cur) => (a += `--${cur[0]}=${cur[1]} `), "") || + ""; + + const command = `${commandline} ${options} ${targetOptions} --args='target_os="${platform}" target_cpu="${target.cpu}" ${common}${args}${targetArgs}'`; + console.log("Command:"); console.log(command); + console.log("==============================="); executeCmdSync(command); return true; } else { console.log( - `Could not find platform "${platform}" for targetCpu "${cpu}" ` + `Could not find platform "${platform}" for target "${targetName}" ` ); return false; } @@ -91,44 +137,45 @@ const configurePlatform = (platform: PlatformName, cpu: string) => { const buildPlatform = ( platform: PlatformName, - cpu: string, + targetName: string, callback: () => void ) => { - console.log(`Building platform "${platform}" for cpu "${cpu}"`); + console.log(`Building platform "${platform}" for target "${targetName}"`); executeCmd( - `ninja -C ${getOutDir(platform, cpu)}`, - `${platform}/${cpu}`, + `ninja -C ${getOutDir(platform, targetName)}`, + `${platform}/${targetName}`, callback ); }; -const processOutput = (platform: PlatformName, cpu: string) => { - console.log(`Copying output for platform "${platform}" and cpu "${cpu}"`); - const source = getOutDir(platform, cpu); - const configuration = configurations[platform]; +const processOutput = (platformName: PlatformName, targetName: string) => { + console.log( + `Copying output for platform "${platformName}" and cpu "${targetName}"` + ); + const source = getOutDir(platformName, targetName); + const configuration = configurations[platformName]; if (configuration) { const libNames = configuration.outputNames; - let target = `${currentDir}/${configurations[platform].outputRoot}/${cpu}`; - // Check if we have any mappings here - if (configuration.outputMapping) { - const indexOfCpu = configuration.cpus.indexOf(cpu); - const mappedTo = configuration.outputMapping[indexOfCpu]; - target = `${currentDir}/${configurations[platform].outputRoot}/${mappedTo}`; + let targetDir = `${currentDir}/${configurations[platformName].outputRoot}/${targetName}`; + // Check if we have any output mappings here + const target = configuration.targets[targetName]; + if (target.output) { + targetDir = `${currentDir}/${configurations[platformName].outputRoot}/${target.output}`; } - if (!fs.existsSync(target)) { - console.log(`Creating directory '${target}'...`); - fs.mkdirSync(target + "/", { recursive: true }); + if (!fs.existsSync(targetDir)) { + console.log(`Creating directory '${targetDir}'...`); + fs.mkdirSync(targetDir + "/", { recursive: true }); } libNames.forEach((libName) => { - console.log(`Copying ${source}/${libName} to ${target}/`); - console.log(`cp ${source}/${libName} ${target}/.`); - executeCmdSync(`cp ${source}/${libName} ${target}/.`); + console.log(`Copying ${source}/${libName} to ${targetDir}/`); + console.log(`cp ${source}/${libName} ${targetDir}/.`); + executeCmdSync(`cp ${source}/${libName} ${targetDir}/.`); }); } else { throw new Error( - `Could not find platform "${platform}" for tagetCpu "${cpu}" ` + `Could not find platform "${platformName}" for tagetCpu "${targetName}" ` ); } }; @@ -140,33 +187,40 @@ try { // Start by running sync executeCmdSync("PATH=../depot_tools/:$PATH python2 tools/git-sync-deps"); console.log("gclient sync done"); - typedKeys(configurations).forEach((platform) => { - if (SelectedPlatform === "" || SelectedPlatform === platform) { - const config = configurations[platform]; - config.cpus.forEach((cpu) => { - if (SelectedCpu === "" || SelectedCpu === cpu) { - try { - // Configure the platform - if (!configurePlatform(platform, cpu)) { - throw Error( - `Error configuring platform "${platform}" for cpu "${cpu}"` - ); - } - // Spawn build - buildPlatform(platform, cpu, () => { - process.chdir(SkiaDir); - // Copy the output - processOutput(platform, cpu); - // Revert back to original directory - process.chdir(currentDir); - }); - } catch (err) { - console.log(`ERROR ${platform}/${cpu}: ${err}`); - } - } - }); + + // Find platform/target + const platform = configurations[SelectedPlatform]; + if (!platform) { + console.log(`Could not find platform ${SelectedPlatform}`); + exit(1); + } + const target = platform.targets[SelectedTarget]; + if (!target) { + console.log( + `Could not find target ${SelectedTarget} for platform ${SelectedPlatform}` + ); + exit(1); + } + + try { + // Configure the platform + if (!configurePlatform(SelectedPlatform, SelectedTarget)) { + throw Error( + `Error configuring platform "${SelectedPlatform}" for cpu "${SelectedTarget}"` + ); } - }); + // Spawn build + buildPlatform(SelectedPlatform, SelectedTarget, () => { + process.chdir(SkiaDir); + // Copy the output + processOutput(SelectedPlatform, SelectedTarget); + // Revert back to original directory + process.chdir(currentDir); + }); + } catch (err) { + console.log(`ERROR ${SelectedPlatform}/${SelectedTarget}: ${err}`); + } + process.chdir(currentDir); } catch (err) { console.log(err); diff --git a/scripts/skia-configuration.ts b/scripts/skia-configuration.ts index 46d3deca83..7e8e7c8600 100644 --- a/scripts/skia-configuration.ts +++ b/scripts/skia-configuration.ts @@ -1,3 +1,5 @@ +import { executeCmdSync } from "./utils"; + const NdkDir: string = process.env.ANDROID_NDK ?? ""; export const commonArgs = [ @@ -19,20 +21,57 @@ export const commonArgs = [ ["is_component_build", false], ]; +// Get paths to iPhone SDKs +export const iPhoneosSdk = executeCmdSync( + "xcrun --sdk iphoneos --show-sdk-path" +) + .toString() + .trim(); + +export const iPhoneSimulatorSdk = executeCmdSync( + "xcrun --sdk iphonesimulator --show-sdk-path" +) + .toString() + .trim(); + export type PlatformName = "ios" | "android"; -type Configuration = { [K in PlatformName]: Platform }; -type Platform = { - cpus: string[]; - args: (string | boolean | number)[][]; +type Arg = (string | boolean | number)[]; +export type Target = { + args?: Arg[]; + cpu: string; + output?: string; + options?: Arg[]; +}; +export type Configuration = { [K in PlatformName]: Platform }; +export type Platform = { + targets: { [key: string]: Target }; + args: Arg[]; outputRoot: string; outputNames: string[]; - outputMapping?: string[]; + options?: Arg[]; }; export const configurations: Configuration = { android: { - cpus: ["arm", "arm64", "x86", "x64"], + targets: { + arm: { + cpu: "arm", + output: "armeabi-v7a", + }, + arm64: { + cpu: "arm64", + output: "arm64-v8a", + }, + x86: { + cpu: "x86", + output: "x86", + }, + x64: { + cpu: "x64", + output: "x86_64", + }, + }, args: [ ["ndk", `"${NdkDir}"`], ["skia_use_system_freetype2", false], @@ -46,21 +85,62 @@ export const configurations: Configuration = { ], outputRoot: "package/libs/android", outputNames: ["libskia.a", "libskshaper.a", "libsvg.a"], - outputMapping: ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"], }, ios: { - cpus: ["arm", "arm64", "x64"], + targets: { + // This one can probably be removed now? + // arm: { + // cpu: "arm", + // args: [ + // ["ios_min_target", '"10.0"'], + // [ + // "extra_cflags", + // '["-DSKIA_C_DLL", "-DHAVE_ARC4RANDOM_BUF", "-target", "arm64-apple-ios"]', + // ], + // ], + // }, + "arm64-iphoneos": { + cpu: "arm64", + args: [ + ["ios_min_target", '"11.0"'], + ["xcode_sysroot", `"${iPhoneosSdk}"`], + ["extra_ldflags", `["--sysroot='${iPhoneosSdk}'"]`], + [ + "extra_cflags", + '["-DSKIA_C_DLL", "-DHAVE_ARC4RANDOM_BUF", "-target", "arm64-apple-ios", "-fembed-bitcode"]', + ], + ], + }, + "arm64-iphonesimulator": { + cpu: "arm64", + args: [ + ["ios_min_target", '"11.0"'], + ["xcode_sysroot", `"${iPhoneSimulatorSdk}"`], + ["extra_ldflags", `["--sysroot='${iPhoneSimulatorSdk}'"]`], + [ + "extra_cflags", + '["-DSKIA_C_DLL", "-DHAVE_ARC4RANDOM_BUF", "-target", "arm64-apple-ios-simulator"]', + ], + ], + }, + x64: { + cpu: "x64", + args: [ + ["ios_min_target", '"11.0"'], + ["xcode_sysroot", `"${iPhoneSimulatorSdk}"`], + ["extra_ldflags", `["--sysroot='${iPhoneSimulatorSdk}'"]`], + [ + "extra_cflags", + '["-DSKIA_C_DLL", "-DHAVE_ARC4RANDOM_BUF", "-target", "arm64-apple-ios-simulator"]', + ], + ], + }, + }, args: [ ["skia_use_metal", true], ["skia_use_gl", true], ["cc", '"clang"'], ["cxx", '"clang++"'], - [ - "extra_cflags", - '["-DSKIA_C_DLL", "-DHAVE_ARC4RANDOM_BUF", "-mios-version-min=10.0"]', - ], - // Can this be removed? Seems like it can - // ["extra_ldflags", '["ios_version_min=10.0"]'], ], outputRoot: "package/libs/ios", outputNames: ["libskia.a", "libskshaper.a", "libsvg.a"], diff --git a/scripts/utils.ts b/scripts/utils.ts index d78d8b2c83..4ddcc0c00a 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -3,7 +3,11 @@ import { exit } from "process"; const fs = require("fs"); export const executeCmdSync = (command: string) => { - execSync(command, { stdio: "inherit", env: process.env }); + try { + return execSync(command); + } catch (e) { + exit(1); + } }; export const executeCmd = ( diff --git a/scripts/workflow-copy-libs.ts b/scripts/workflow-copy-libs.ts index 97cb399d45..05e9ed73ed 100644 --- a/scripts/workflow-copy-libs.ts +++ b/scripts/workflow-copy-libs.ts @@ -36,10 +36,10 @@ import { ensureFolderExists } from "./utils"; * libskshaper.a * libsvg.a * - * ./skia-ios-fat-libs: - * libskia.a - * libskshaper.a - * libsvg.a + * ./skia-ios-xc-frameworks: + * libskia.xcframework + * libskshaper.xcframework + * libsvg.xcframework */ console.log("Copying Skia Binaries from artifacts to libs folder"); @@ -89,6 +89,6 @@ destinations.forEach((d, i) => { }); console.log("Copying ios files..."); -copyFiles("skia-ios-fat-libs", "./package/libs/ios"); +copyFiles("skia-ios-xcframeworks", "./package/libs/ios"); console.log("Done copying artifacts.");