@shopify/react-native-skia 2.3.5 → 2.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import https from "https";
3
3
  import path from "path";
4
4
  import os from "os";
5
+ import crypto from "crypto";
5
6
  import { spawn } from "child_process";
6
7
  import { fileURLToPath } from "url";
7
8
 
@@ -9,10 +10,34 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
10
 
10
11
  const repo = "shopify/react-native-skia";
11
12
 
12
- const getSkiaVersion = () => {
13
- const packageJsonPath = path.join(__dirname, "..", "package.json");
14
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
15
- return packageJson.skiaVersion;
13
+ const packageJsonPath = path.join(__dirname, "..", "package.json");
14
+
15
+ const getPackageJson = () => {
16
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
17
+ };
18
+
19
+ const getSkiaConfig = (graphite = false) => {
20
+ const packageJson = getPackageJson();
21
+ return graphite ? packageJson["skia-graphite"] : packageJson.skia;
22
+ };
23
+
24
+ const getSkiaVersion = (graphite = false) => {
25
+ const packageJson = getPackageJson();
26
+ const skiaConfig = getSkiaConfig(graphite);
27
+ return skiaConfig?.version || packageJson.skiaVersion;
28
+ };
29
+
30
+ const updateSkiaChecksums = (checksums, graphite = false) => {
31
+ const packageJson = getPackageJson();
32
+ const field = graphite ? "skia-graphite" : "skia";
33
+
34
+ if (!packageJson[field]) {
35
+ packageJson[field] = { version: packageJson[field]?.version || "m142", checksums: {} };
36
+ }
37
+
38
+ packageJson[field].checksums = checksums;
39
+
40
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf8");
16
41
  };
17
42
 
18
43
  const GRAPHITE = !!process.env.SK_GRAPHITE;
@@ -25,13 +50,13 @@ const names = [
25
50
  `${prefix}-apple-xcframeworks`,
26
51
  ];
27
52
  if (GRAPHITE) {
28
- names.push("skia-graphite-headers");
53
+ names.push(`${prefix}-headers`);
29
54
  }
30
55
 
31
- const skiaVersion = getSkiaVersion();
32
- const releaseTag = `skia-${skiaVersion}`;
56
+ const skiaVersion = getSkiaVersion(GRAPHITE);
57
+ const releaseTag = GRAPHITE ? `skia-graphite-${skiaVersion}` : `skia-${skiaVersion}`;
33
58
  console.log(
34
- `📦 Downloading Skia prebuilt binaries for version: ${skiaVersion}`
59
+ `📦 Downloading Skia prebuilt binaries for ${releaseTag}`
35
60
  );
36
61
 
37
62
  const runCommand = (command, args) => {
@@ -52,64 +77,89 @@ const runCommand = (command, args) => {
52
77
  });
53
78
  };
54
79
 
55
- const downloadToFile = (url, destPath) => {
80
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
81
+
82
+ const downloadToFile = (url, destPath, maxRetries = 5) => {
56
83
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
57
- return new Promise((resolve, reject) => {
58
- const request = (currentUrl) => {
59
- https
60
- .get(currentUrl, { headers: { "User-Agent": "node" } }, (res) => {
61
- if (
62
- res.statusCode &&
63
- [301, 302, 303, 307, 308].includes(res.statusCode)
64
- ) {
65
- const { location } = res.headers;
66
- if (location) {
67
- res.resume();
68
- request(location);
69
- } else {
70
- reject(new Error(`Redirect without location for ${currentUrl}`));
84
+
85
+ const attemptDownload = (retryCount = 0) => {
86
+ return new Promise((resolve, reject) => {
87
+ const request = (currentUrl) => {
88
+ https
89
+ .get(currentUrl, { headers: { "User-Agent": "node" } }, (res) => {
90
+ if (
91
+ res.statusCode &&
92
+ [301, 302, 303, 307, 308].includes(res.statusCode)
93
+ ) {
94
+ const { location } = res.headers;
95
+ if (location) {
96
+ res.resume();
97
+ request(location);
98
+ } else {
99
+ reject(new Error(`Redirect without location for ${currentUrl}`));
100
+ }
101
+ return;
71
102
  }
72
- return;
73
- }
74
103
 
75
- if (res.statusCode !== 200) {
76
- reject(
77
- new Error(
104
+ if (res.statusCode !== 200) {
105
+ const error = new Error(
78
106
  `Failed to download: ${res.statusCode} ${res.statusMessage}`
79
- )
80
- );
81
- res.resume();
82
- return;
83
- }
84
-
85
- const fileStream = fs.createWriteStream(destPath);
86
- res.pipe(fileStream);
107
+ );
108
+ error.statusCode = res.statusCode;
109
+ reject(error);
110
+ res.resume();
111
+ return;
112
+ }
87
113
 
88
- fileStream.on("finish", () => {
89
- fileStream.close((err) => {
90
- if (err) {
91
- // If closing the stream errors, perform the same cleanup and reject.
92
- fileStream.destroy();
93
- fs.unlink(destPath, () => reject(err));
94
- } else {
95
- resolve();
96
- }
114
+ const fileStream = fs.createWriteStream(destPath);
115
+ res.pipe(fileStream);
116
+
117
+ fileStream.on("finish", () => {
118
+ fileStream.close((err) => {
119
+ if (err) {
120
+ // If closing the stream errors, perform the same cleanup and reject.
121
+ fileStream.destroy();
122
+ fs.unlink(destPath, () => reject(err));
123
+ } else {
124
+ resolve();
125
+ }
126
+ });
97
127
  });
98
- });
99
128
 
100
- const cleanup = (error) => {
101
- fileStream.destroy();
102
- fs.unlink(destPath, () => reject(error));
103
- };
129
+ const cleanup = (error) => {
130
+ fileStream.destroy();
131
+ fs.unlink(destPath, () => reject(error));
132
+ };
104
133
 
105
- res.on("error", cleanup);
106
- fileStream.on("error", cleanup);
107
- })
108
- .on("error", reject);
109
- };
134
+ res.on("error", cleanup);
135
+ fileStream.on("error", cleanup);
136
+ })
137
+ .on("error", reject);
138
+ };
110
139
 
111
- request(url);
112
- });
140
+ request(url);
141
+ });
142
+ };
143
+
144
+ const downloadWithRetry = async (retryCount = 0) => {
145
+ try {
146
+ await attemptDownload(retryCount);
147
+ } catch (error) {
148
+ const isRateLimit = error.statusCode === 403 || error.message.includes('rate limit');
149
+ const shouldRetry = retryCount < maxRetries && (isRateLimit || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT');
150
+
151
+ if (shouldRetry) {
152
+ const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s, 8s, 16s
153
+ console.log(` ⚠️ Download failed (${error.message}), retrying in ${delay/1000}s... (attempt ${retryCount + 1}/${maxRetries})`);
154
+ await sleep(delay);
155
+ return downloadWithRetry(retryCount + 1);
156
+ } else {
157
+ throw error;
158
+ }
159
+ }
160
+ };
161
+
162
+ return downloadWithRetry();
113
163
  };
114
164
 
115
165
  const extractTarGz = async (archivePath, destDir) => {
@@ -178,6 +228,92 @@ const artifactsDir = path.resolve(
178
228
 
179
229
  const libsDir = path.resolve(__dirname, "../libs");
180
230
 
231
+ // Function to calculate the checksum of a directory
232
+ const calculateDirectoryChecksum = (directory) => {
233
+ if (!fs.existsSync(directory)) {
234
+ return null;
235
+ }
236
+
237
+ const hash = crypto.createHash("sha256");
238
+ const files = [];
239
+
240
+ const collectFiles = (dir) => {
241
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
242
+ for (const entry of entries) {
243
+ const fullPath = path.join(dir, entry.name);
244
+ if (entry.isDirectory()) {
245
+ collectFiles(fullPath);
246
+ } else if (entry.isFile()) {
247
+ files.push(fullPath);
248
+ }
249
+ }
250
+ };
251
+
252
+ collectFiles(directory);
253
+ files.sort();
254
+
255
+ for (const file of files) {
256
+ const relativePath = path.relative(directory, file);
257
+ hash.update(relativePath);
258
+ hash.update(fs.readFileSync(file));
259
+ }
260
+
261
+ return hash.digest("hex");
262
+ };
263
+
264
+ // Function to calculate all library checksums
265
+ const calculateLibraryChecksums = () => {
266
+ const checksums = {};
267
+
268
+ // Android architectures
269
+ const androidArchs = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"];
270
+ for (const arch of androidArchs) {
271
+ const archDir = path.join(libsDir, "android", arch);
272
+ const checksum = calculateDirectoryChecksum(archDir);
273
+ if (checksum) {
274
+ const checksumKey = `android-${arch}`;
275
+ checksums[checksumKey] = checksum;
276
+ }
277
+ }
278
+
279
+ // Apple frameworks
280
+ const appleDir = path.join(libsDir, "apple");
281
+ const appleChecksum = calculateDirectoryChecksum(appleDir);
282
+ if (appleChecksum) {
283
+ checksums["apple-xcframeworks"] = appleChecksum;
284
+ }
285
+
286
+ return checksums;
287
+ };
288
+
289
+ // Function to verify if checksums match
290
+ const verifyChecksums = () => {
291
+ const skiaConfig = getSkiaConfig(GRAPHITE);
292
+ const expectedChecksums = skiaConfig?.checksums || {};
293
+
294
+ // If no checksums in package.json, we need to download
295
+ if (Object.keys(expectedChecksums).length === 0) {
296
+ console.log("⚠️ No checksums found in package.json");
297
+ return false;
298
+ }
299
+
300
+ const actualChecksums = calculateLibraryChecksums();
301
+
302
+ // Check if all expected checksums match
303
+ for (const [key, expectedChecksum] of Object.entries(expectedChecksums)) {
304
+ const actualChecksum = actualChecksums[key];
305
+ if (actualChecksum !== expectedChecksum) {
306
+ console.log(`⚠️ Checksum mismatch for ${key}`);
307
+ console.log(` Expected: ${expectedChecksum}`);
308
+ console.log(` Actual: ${actualChecksum || "missing"}`);
309
+ return false;
310
+ }
311
+ }
312
+
313
+ console.log("✅ All checksums match");
314
+ return true;
315
+ };
316
+
181
317
  // Function to check if prebuilt binaries are already installed
182
318
  const areBinariesInstalled = () => {
183
319
  if (!fs.existsSync(libsDir)) {
@@ -223,17 +359,14 @@ const clearDirectory = (directory) => {
223
359
  };
224
360
 
225
361
  const main = async () => {
226
- const forceReinstall = process.argv.includes("--force");
227
-
228
- // Check if binaries are already installed
229
- if (!forceReinstall && areBinariesInstalled()) {
230
- console.log("✅ Prebuilt binaries already installed, skipping download");
231
- console.log(" Use --force to reinstall");
362
+ // Check if binaries are installed and checksums match
363
+ if (areBinariesInstalled() && verifyChecksums()) {
364
+ console.log("✅ Prebuilt binaries already installed with matching checksums, skipping download");
232
365
  return;
233
366
  }
234
367
 
235
- if (forceReinstall) {
236
- console.log("🔄 Force reinstall requested");
368
+ if (areBinariesInstalled()) {
369
+ console.log("⚠️ Binaries installed but checksums don't match, re-downloading...");
237
370
  }
238
371
 
239
372
  console.log("🧹 Clearing existing artifacts...");
@@ -270,22 +403,50 @@ const main = async () => {
270
403
  fs.mkdirSync(androidDir, { recursive: true });
271
404
 
272
405
  // Copy android artifacts
273
- const androidArchs = [
274
- { src: "skia-android-arm", dest: "armeabi-v7a" },
275
- { src: "skia-android-arm-64", dest: "arm64-v8a" },
276
- { src: "skia-android-arm-x86", dest: "x86" },
277
- { src: "skia-android-arm-x64", dest: "x86_64" },
406
+ const androidArchs = GRAPHITE ? [
407
+ { artifact: `${prefix}-android-arm`, srcSubdir: "arm", dest: "armeabi-v7a" },
408
+ { artifact: `${prefix}-android-arm-64`, srcSubdir: "arm64", dest: "arm64-v8a" },
409
+ { artifact: `${prefix}-android-arm-x86`, srcSubdir: "x86", dest: "x86" },
410
+ { artifact: `${prefix}-android-arm-x64`, srcSubdir: "x64", dest: "x86_64" },
411
+ ] : [
412
+ { artifact: `${prefix}-android-arm`, srcSubdir: "armeabi-v7a", dest: "armeabi-v7a" },
413
+ { artifact: `${prefix}-android-arm-64`, srcSubdir: "arm64-v8a", dest: "arm64-v8a" },
414
+ { artifact: `${prefix}-android-arm-x86`, srcSubdir: "x86", dest: "x86" },
415
+ { artifact: `${prefix}-android-arm-x64`, srcSubdir: "x86_64", dest: "x86_64" },
278
416
  ];
279
417
 
280
- androidArchs.forEach(({ src, dest }) => {
281
- // The tar file extracts to artifactName/dest (e.g., skia-android-arm/armeabi-v7a)
282
- const srcDir = path.join(artifactsDir, src, dest);
418
+ androidArchs.forEach(({ artifact, srcSubdir, dest }) => {
419
+ // The tar file extracts to artifactName/srcSubdir
420
+ const srcDir = path.join(artifactsDir, artifact, srcSubdir);
283
421
  const destDir = path.join(androidDir, dest);
422
+ console.log(` Checking ${srcDir} -> ${destDir}`);
284
423
  if (fs.existsSync(srcDir)) {
424
+ console.log(` ✓ Copying ${artifact}/${srcSubdir}`);
285
425
  fs.mkdirSync(destDir, { recursive: true });
286
- fs.readdirSync(srcDir).forEach((file) => {
287
- fs.copyFileSync(path.join(srcDir, file), path.join(destDir, file));
288
- });
426
+
427
+ const copyDir = (srcPath, destPath) => {
428
+ fs.mkdirSync(destPath, { recursive: true });
429
+ fs.readdirSync(srcPath).forEach((file) => {
430
+ const srcFile = path.join(srcPath, file);
431
+ const destFile = path.join(destPath, file);
432
+ const stat = fs.lstatSync(srcFile);
433
+
434
+ // Skip sockets and other special files
435
+ if (stat.isSocket() || stat.isFIFO() || stat.isCharacterDevice() || stat.isBlockDevice()) {
436
+ return;
437
+ }
438
+
439
+ if (stat.isDirectory()) {
440
+ copyDir(srcFile, destFile);
441
+ } else {
442
+ fs.copyFileSync(srcFile, destFile);
443
+ }
444
+ });
445
+ };
446
+
447
+ copyDir(srcDir, destDir);
448
+ } else {
449
+ console.log(` ✗ Source directory not found: ${srcDir}`);
289
450
  }
290
451
  });
291
452
 
@@ -294,7 +455,7 @@ const main = async () => {
294
455
  // The tar file extracts to skia-apple-xcframeworks/apple
295
456
  const appleSrcDir = path.join(
296
457
  artifactsDir,
297
- "skia-apple-xcframeworks",
458
+ `${prefix}-apple-xcframeworks`,
298
459
  "apple"
299
460
  );
300
461
  if (fs.existsSync(appleSrcDir)) {
@@ -326,7 +487,90 @@ const main = async () => {
326
487
  });
327
488
  }
328
489
 
329
- console.log("✅ Done");
490
+ // Copy Graphite headers if using Graphite
491
+ if (GRAPHITE) {
492
+ console.log("📦 Copying Graphite headers...");
493
+ const cppDir = path.resolve(__dirname, "../cpp");
494
+ const headersSrcDir = path.join(artifactsDir, `${prefix}-headers`);
495
+
496
+ console.log(` Looking for headers in: ${headersSrcDir}`);
497
+ console.log(` Headers dir exists: ${fs.existsSync(headersSrcDir)}`);
498
+
499
+ if (fs.existsSync(headersSrcDir)) {
500
+ console.log(` Contents: ${fs.readdirSync(headersSrcDir).join(", ")}`);
501
+
502
+ // The asset contains packages/skia/cpp structure, so we need to navigate into it
503
+ const packagesDir = path.join(headersSrcDir, "packages", "skia", "cpp");
504
+ console.log(` Looking for packages dir: ${packagesDir}`);
505
+ console.log(` Packages dir exists: ${fs.existsSync(packagesDir)}`);
506
+
507
+ if (fs.existsSync(packagesDir)) {
508
+ console.log(` Packages contents: ${fs.readdirSync(packagesDir).join(", ")}`);
509
+
510
+ // Copy dawn/include
511
+ const dawnIncludeSrc = path.join(packagesDir, "dawn", "include");
512
+ const dawnIncludeDest = path.join(cppDir, "dawn", "include");
513
+ console.log(` Dawn source: ${dawnIncludeSrc}`);
514
+ console.log(` Dawn source exists: ${fs.existsSync(dawnIncludeSrc)}`);
515
+
516
+ if (fs.existsSync(dawnIncludeSrc)) {
517
+ const copyDir = (src, dest) => {
518
+ fs.mkdirSync(dest, { recursive: true });
519
+ fs.readdirSync(src).forEach((file) => {
520
+ const srcFile = path.join(src, file);
521
+ const destFile = path.join(dest, file);
522
+ if (fs.lstatSync(srcFile).isDirectory()) {
523
+ copyDir(srcFile, destFile);
524
+ } else {
525
+ fs.copyFileSync(srcFile, destFile);
526
+ }
527
+ });
528
+ };
529
+ copyDir(dawnIncludeSrc, dawnIncludeDest);
530
+ console.log(" ✓ Dawn headers copied");
531
+ } else {
532
+ console.log(" ✗ Dawn headers not found");
533
+ }
534
+
535
+ // Copy graphite headers
536
+ const graphiteSrc = path.join(packagesDir, "skia", "src", "gpu", "graphite");
537
+ const graphiteDest = path.join(cppDir, "skia", "src", "gpu", "graphite");
538
+ console.log(` Graphite source: ${graphiteSrc}`);
539
+ console.log(` Graphite source exists: ${fs.existsSync(graphiteSrc)}`);
540
+
541
+ if (fs.existsSync(graphiteSrc)) {
542
+ const copyDir = (src, dest) => {
543
+ fs.mkdirSync(dest, { recursive: true });
544
+ fs.readdirSync(src).forEach((file) => {
545
+ const srcFile = path.join(src, file);
546
+ const destFile = path.join(dest, file);
547
+ if (fs.lstatSync(srcFile).isDirectory()) {
548
+ copyDir(srcFile, destFile);
549
+ } else {
550
+ fs.copyFileSync(srcFile, destFile);
551
+ }
552
+ });
553
+ };
554
+ copyDir(graphiteSrc, graphiteDest);
555
+ console.log(" ✓ Graphite headers copied");
556
+ } else {
557
+ console.log(" ✗ Graphite headers not found");
558
+ }
559
+ } else {
560
+ console.log(" ✗ Packages directory not found in headers asset");
561
+ }
562
+ } else {
563
+ console.log(" ✗ Headers directory not found");
564
+ }
565
+ }
566
+
567
+ console.log("✅ Completed installation of Skia prebuilt binaries.");
568
+
569
+ // Calculate and update checksums in package.json
570
+ console.log("🔐 Calculating and updating checksums...");
571
+ const newChecksums = calculateLibraryChecksums();
572
+ updateSkiaChecksums(newChecksums, GRAPHITE);
573
+ console.log("✅ Checksums updated in package.json");
330
574
 
331
575
  // Clean up artifacts directory
332
576
  console.log("🗑️ Cleaning up artifacts directory...");
@@ -338,4 +582,4 @@ const main = async () => {
338
582
  main().catch((error) => {
339
583
  console.error("❌ Error:", error);
340
584
  process.exit(1);
341
- });
585
+ });
@@ -14,7 +14,7 @@ import type { SkColor } from "./Color";
14
14
  import type { InputRRect } from "./RRect";
15
15
  import type { BlendMode } from "./Paint/BlendMode";
16
16
  import type { SkPoint, PointMode } from "./Point";
17
- import type { InputMatrix } from "./Matrix";
17
+ import type { InputMatrix, SkMatrix } from "./Matrix";
18
18
  import type { SkImageFilter } from "./ImageFilter";
19
19
  import type { SkVertices } from "./Vertices";
20
20
  import type { SkTextBlob } from "./TextBlob";
@@ -241,6 +241,12 @@ export interface SkCanvas {
241
241
  */
242
242
  restoreToCount(saveCount: number): void;
243
243
 
244
+ /**
245
+ * Legacy version of getLocalToDevice(), which strips away any Z information, and
246
+ * just returns a 3x3 version.
247
+ */
248
+ getTotalMatrix(): SkMatrix;
249
+
244
250
  /**
245
251
  * Draws the given points using the current clip, current matrix, and the provided paint.
246
252
  *
@@ -218,6 +218,13 @@ export class JsiSkCanvas
218
218
  this.ref.restoreToCount(saveCount);
219
219
  }
220
220
 
221
+ getTotalMatrix(): SkMatrix {
222
+ return new JsiSkMatrix(
223
+ this.CanvasKit,
224
+ Float32Array.of(...this.ref.getTotalMatrix())
225
+ );
226
+ }
227
+
221
228
  drawPoints(mode: PointMode, points: SkPoint[], paint: SkPaint) {
222
229
  this.ref.drawPoints(
223
230
  getEnum(this.CanvasKit, "PointMode", mode),