@cloudcare/cocos-sdk 0.1.0-alpha.3 → 0.1.0-alpha.5
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.
- package/README.md +37 -1
- package/bin/cli.cjs +14 -0
- package/components/creator2/ReplayPrivacy.ts +23 -0
- package/components/creator3/ReplayPrivacy.ts +25 -0
- package/dist/creator2/core/replay-async-transport.d.ts +4 -0
- package/dist/creator2/core/replay-async-transport.d.ts.map +1 -0
- package/dist/creator2/core/replay-async-transport.js +67 -0
- package/dist/creator2/core/replay-async-transport.js.map +1 -0
- package/dist/creator2/core/replay-file.d.ts +5 -0
- package/dist/creator2/core/replay-file.d.ts.map +1 -0
- package/dist/creator2/core/replay-file.js +101 -0
- package/dist/creator2/core/replay-file.js.map +1 -0
- package/dist/creator2/core/replay-privacy-nodes.d.ts +14 -0
- package/dist/creator2/core/replay-privacy-nodes.d.ts.map +1 -0
- package/dist/creator2/core/replay-privacy-nodes.js +18 -0
- package/dist/creator2/core/replay-privacy-nodes.js.map +1 -0
- package/dist/creator2/core/replay.d.ts +5 -3
- package/dist/creator2/core/replay.d.ts.map +1 -1
- package/dist/creator2/core/replay.js +74 -41
- package/dist/creator2/core/replay.js.map +1 -1
- package/dist/creator2/core/transport.d.ts +1 -0
- package/dist/creator2/core/transport.d.ts.map +1 -1
- package/dist/creator2/core/transport.js.map +1 -1
- package/dist/creator2/core/version.d.ts +1 -1
- package/dist/creator2/core/version.js +1 -1
- package/dist/creator2/creator2/capture.d.ts +1 -1
- package/dist/creator2/creator2/capture.d.ts.map +1 -1
- package/dist/creator2/creator2/capture.js +9 -16
- package/dist/creator2/creator2/capture.js.map +1 -1
- package/dist/creator2/creator2/transport.d.ts +1 -0
- package/dist/creator2/creator2/transport.d.ts.map +1 -1
- package/dist/creator2/creator2/transport.js +4 -0
- package/dist/creator2/creator2/transport.js.map +1 -1
- package/dist/creator3/core/replay-async-transport.d.ts +4 -0
- package/dist/creator3/core/replay-async-transport.d.ts.map +1 -0
- package/dist/creator3/core/replay-async-transport.js +16 -0
- package/dist/creator3/core/replay-async-transport.js.map +1 -0
- package/dist/creator3/core/replay-file.d.ts +5 -0
- package/dist/creator3/core/replay-file.d.ts.map +1 -0
- package/dist/creator3/core/replay-file.js +31 -0
- package/dist/creator3/core/replay-file.js.map +1 -0
- package/dist/creator3/core/replay-privacy-nodes.d.ts +14 -0
- package/dist/creator3/core/replay-privacy-nodes.d.ts.map +1 -0
- package/dist/creator3/core/replay-privacy-nodes.js +15 -0
- package/dist/creator3/core/replay-privacy-nodes.js.map +1 -0
- package/dist/creator3/core/replay.d.ts +5 -3
- package/dist/creator3/core/replay.d.ts.map +1 -1
- package/dist/creator3/core/replay.js +23 -9
- package/dist/creator3/core/replay.js.map +1 -1
- package/dist/creator3/core/transport.d.ts +1 -0
- package/dist/creator3/core/transport.d.ts.map +1 -1
- package/dist/creator3/core/transport.js.map +1 -1
- package/dist/creator3/core/version.d.ts +1 -1
- package/dist/creator3/core/version.js +1 -1
- package/dist/creator3/creator3/capture.d.ts +1 -1
- package/dist/creator3/creator3/capture.d.ts.map +1 -1
- package/dist/creator3/creator3/capture.js +6 -13
- package/dist/creator3/creator3/capture.js.map +1 -1
- package/dist/creator3/creator3/transport.d.ts +1 -0
- package/dist/creator3/creator3/transport.d.ts.map +1 -1
- package/dist/creator3/creator3/transport.js +4 -0
- package/dist/creator3/creator3/transport.js.map +1 -1
- package/extensions/creator2/install-native.cjs +126 -6
- package/extensions/creator2/install-spm.cjs +190 -0
- package/extensions/creator3/install-native.cjs +126 -6
- package/extensions/creator3/install-spm.cjs +190 -0
- package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosBridge.java +4 -0
- package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosReplayImageJobs.java +55 -0
- package/native/ios/FTCocosBridge.m +26 -2
- package/native/ios/FTCocosBridge.podspec +4 -4
- package/native/ios/FTCocosReplayImageJobs.h +11 -0
- package/native/ios/FTCocosReplayImageJobs.m +54 -0
- package/native/ios/Package.swift +26 -0
- package/native/ios/include/FTCocosBridge.h +2 -0
- package/native/shared/FTReplayFileBridge.h +62 -0
- package/native/shared/FTReplayFileWorker.h +88 -0
- package/package.json +2 -1
|
@@ -22,6 +22,7 @@ function installNative(buildRoot, extensionRoot, logger = console) {
|
|
|
22
22
|
buildRoot = path.resolve(buildRoot);
|
|
23
23
|
extensionRoot = path.resolve(extensionRoot);
|
|
24
24
|
if (!fs.existsSync(buildRoot)) return;
|
|
25
|
+
const dependencyManager = readIosDependencyManager(buildRoot, extensionRoot);
|
|
25
26
|
const nativeSource = path.join(extensionRoot, 'native');
|
|
26
27
|
if (!fs.existsSync(nativeSource)) {
|
|
27
28
|
logger.warn(`[cocos-sdk] Native bridge not found at ${nativeSource}`);
|
|
@@ -30,6 +31,11 @@ function installNative(buildRoot, extensionRoot, logger = console) {
|
|
|
30
31
|
const buildNative = path.join(buildRoot, 'cocos-sdk-native');
|
|
31
32
|
copyDirectory(nativeSource, buildNative);
|
|
32
33
|
const files = findFiles(buildRoot, 7);
|
|
34
|
+
const replayHeader = path.join(buildNative, 'shared', 'FTReplayFileBridge.h');
|
|
35
|
+
if (fs.existsSync(replayHeader)) {
|
|
36
|
+
findReplayEntryFiles(files)
|
|
37
|
+
.forEach((file) => patchReplayFileBridge(file, replayHeader));
|
|
38
|
+
}
|
|
33
39
|
const gradlePropertiesFiles = files.filter((file) => path.basename(file) === 'gradle.properties');
|
|
34
40
|
const gradleFiles = uniqueFiles([
|
|
35
41
|
...files.filter((file) => /(?:^|\/)app\/build\.gradle$/.test(normalize(file))),
|
|
@@ -42,7 +48,11 @@ function installNative(buildRoot, extensionRoot, logger = console) {
|
|
|
42
48
|
const iosApplicationProjects = xcodeProjects
|
|
43
49
|
.map((projectFile) => ({ projectFile, target: findIosApplicationTarget(projectFile) }))
|
|
44
50
|
.filter((project) => project.target);
|
|
45
|
-
if (
|
|
51
|
+
if (dependencyManager === 'spm' && iosApplicationProjects.length > 0) {
|
|
52
|
+
migrateManagedPods(podfiles, logger);
|
|
53
|
+
copyDirectory(path.join(nativeSource, 'ios'), path.join(buildNative, 'FTCocosBridge'));
|
|
54
|
+
}
|
|
55
|
+
if (dependencyManager === 'cocoapods' && podfiles.length === 0) {
|
|
46
56
|
iosApplicationProjects.forEach(({ projectFile, target }) => {
|
|
47
57
|
const projectDirectory = path.dirname(path.dirname(projectFile));
|
|
48
58
|
const podfile = path.join(projectDirectory, 'Podfile');
|
|
@@ -52,15 +62,67 @@ function installNative(buildRoot, extensionRoot, logger = console) {
|
|
|
52
62
|
}
|
|
53
63
|
iosApplicationProjects.forEach(({ projectFile, target }) => {
|
|
54
64
|
patchCocos2IosConfiguration(projectFile, target);
|
|
65
|
+
if (dependencyManager === 'spm') {
|
|
66
|
+
require('./install-spm.cjs').installSwiftPackage(projectFile, path.join(buildNative, 'FTCocosBridge'), 'FTCocosBridge', target);
|
|
67
|
+
} else if (fs.readFileSync(projectFile, 'utf8').includes('XCLocalSwiftPackageReference')) {
|
|
68
|
+
require('./install-spm.cjs').removeSwiftPackages(projectFile);
|
|
69
|
+
}
|
|
55
70
|
});
|
|
56
71
|
gradleFiles.forEach((file) => patchGradle(file, buildNative));
|
|
57
72
|
gradlePropertiesFiles.forEach(patchGradleProperties);
|
|
58
|
-
podfiles.forEach((file) => patchPodfile(file, buildNative));
|
|
73
|
+
if (dependencyManager === 'cocoapods') podfiles.forEach((file) => patchPodfile(file, buildNative));
|
|
59
74
|
logger.info(
|
|
60
|
-
`[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${podfiles.length} iOS project files).`,
|
|
75
|
+
`[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${dependencyManager === 'spm' ? iosApplicationProjects.length : podfiles.length} iOS project files; iOS: ${dependencyManager}).`,
|
|
61
76
|
);
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
function patchReplayFileBridge(file, header) {
|
|
80
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
81
|
+
const includeBegin = '// COCOS_SDK_REPLAY_INCLUDE_BEGIN';
|
|
82
|
+
const includeEnd = '// COCOS_SDK_REPLAY_INCLUDE_END';
|
|
83
|
+
const initBegin = '// COCOS_SDK_REPLAY_INIT_BEGIN';
|
|
84
|
+
const initEnd = '// COCOS_SDK_REPLAY_INIT_END';
|
|
85
|
+
// Creator 3 shares Game.cpp across iOS/Android builds. Keep its include stable
|
|
86
|
+
// when another platform's build directory is cleaned or regenerated.
|
|
87
|
+
const sharedDirectory = path.join(path.dirname(file), 'cocos-sdk-replay');
|
|
88
|
+
const include = `${includeBegin}\n#include "cocos-sdk-replay/FTReplayFileBridge.h"\n${includeEnd}`;
|
|
89
|
+
const init = `${initBegin}\n ft_cocos::installReplayFileBridge();\n ${initEnd}\n `;
|
|
90
|
+
let next = original;
|
|
91
|
+
if (markedPattern(initBegin, initEnd).test(next)) {
|
|
92
|
+
next = next.replace(markedPattern(initBegin, initEnd), init.trimEnd());
|
|
93
|
+
} else {
|
|
94
|
+
const anchor = path.basename(file) === 'Game.cpp'
|
|
95
|
+
? /\b(?:return\s+)?BaseGame::init\(\);/
|
|
96
|
+
: /\b(?:return\s+)?se->start\(\);/;
|
|
97
|
+
if (!anchor.test(next)) return;
|
|
98
|
+
next = next.replace(anchor, (value) => init + value);
|
|
99
|
+
}
|
|
100
|
+
next = markedPattern(includeBegin, includeEnd).test(next)
|
|
101
|
+
? next.replace(markedPattern(includeBegin, includeEnd), include)
|
|
102
|
+
: `${include}\n${next}`;
|
|
103
|
+
copyDirectory(path.dirname(header), sharedDirectory);
|
|
104
|
+
if (next !== original) fs.writeFileSync(file, next);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findReplayEntryFiles(files) {
|
|
108
|
+
const entries = files.filter((file) => ['AppDelegate.cpp', 'Game.cpp'].includes(path.basename(file)));
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
let nativeDirectory;
|
|
111
|
+
if (path.basename(file) === 'gradle.properties') {
|
|
112
|
+
const value = readProperty(fs.readFileSync(file, 'utf8'), 'NATIVE_DIR');
|
|
113
|
+
if (value) nativeDirectory = path.resolve(path.dirname(file), value);
|
|
114
|
+
} else if (path.basename(file) === 'CMakeCache.txt') {
|
|
115
|
+
const match = fs.readFileSync(file, 'utf8').match(/^CMAKE_HOME_DIRECTORY:INTERNAL=(.+)$/m);
|
|
116
|
+
if (match) nativeDirectory = match[1].trim();
|
|
117
|
+
}
|
|
118
|
+
if (nativeDirectory) {
|
|
119
|
+
const game = path.resolve(nativeDirectory, '..', 'common', 'Classes', 'Game.cpp');
|
|
120
|
+
if (fs.existsSync(game)) entries.push(game);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return uniqueFiles(entries);
|
|
124
|
+
}
|
|
125
|
+
|
|
64
126
|
function patchCocos2IosConfiguration(projectFile, target) {
|
|
65
127
|
const projectDirectory = path.dirname(path.dirname(projectFile));
|
|
66
128
|
const iosDirectory = path.join(projectDirectory, 'ios');
|
|
@@ -200,9 +262,9 @@ function patchGradle(file, nativeRoot) {
|
|
|
200
262
|
" maven { url 'https://mvnrepo.guance.com/repository/maven-releases' }",
|
|
201
263
|
'}',
|
|
202
264
|
'dependencies {',
|
|
203
|
-
" implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-
|
|
265
|
+
" implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-alpha03'",
|
|
204
266
|
" implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-native:1.1.3'",
|
|
205
|
-
" implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-session-replay:0.1.9-
|
|
267
|
+
" implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-session-replay:0.1.9-alpha03'",
|
|
206
268
|
" implementation 'com.google.code.gson:gson:2.10.1'",
|
|
207
269
|
" implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.22')",
|
|
208
270
|
" implementation 'androidx.appcompat:appcompat:1.1.0'",
|
|
@@ -246,6 +308,10 @@ function replaceIndentedMarkedBlock(contents, begin, end, block) {
|
|
|
246
308
|
|
|
247
309
|
function findIosApplicationTarget(projectFile) {
|
|
248
310
|
const contents = fs.readFileSync(projectFile, 'utf8');
|
|
311
|
+
if (contents.includes('COCOS_SDK_XCODE_PROJECT')) {
|
|
312
|
+
const { readProject, applicationTarget } = require('./install-spm.cjs');
|
|
313
|
+
return applicationTarget(readProject(projectFile))?.[1].name || null;
|
|
314
|
+
}
|
|
249
315
|
const nativeTarget = /\/\* ([^*]+) \*\/ = \{\s*isa = PBXNativeTarget;([\s\S]*?)\n\s*\};/g;
|
|
250
316
|
const targets = [];
|
|
251
317
|
let match;
|
|
@@ -330,4 +396,58 @@ function escapeGroovy(value) { return value.replace(/\\/g, '\\\\').replace(/'/g,
|
|
|
330
396
|
function escapeRuby(value) { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }
|
|
331
397
|
function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
332
398
|
|
|
333
|
-
|
|
399
|
+
function readIosDependencyManager(buildRoot, extensionRoot) {
|
|
400
|
+
for (const start of [buildRoot, extensionRoot]) {
|
|
401
|
+
let directory = path.resolve(start);
|
|
402
|
+
while (true) {
|
|
403
|
+
const file = path.join(directory, 'cocos-sdk.config.json');
|
|
404
|
+
if (fs.existsSync(file)) {
|
|
405
|
+
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
406
|
+
const manager = config.ios?.dependencyManager ?? 'cocoapods';
|
|
407
|
+
if (!['spm', 'cocoapods'].includes(manager)) {
|
|
408
|
+
throw new Error(`[cocos-sdk] Invalid ios.dependencyManager in ${file}: expected spm or cocoapods.`);
|
|
409
|
+
}
|
|
410
|
+
return manager;
|
|
411
|
+
}
|
|
412
|
+
const parent = path.dirname(directory);
|
|
413
|
+
if (parent === directory) break;
|
|
414
|
+
directory = parent;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return 'cocoapods';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function migrateManagedPods(podfiles, logger) {
|
|
421
|
+
for (const file of podfiles) {
|
|
422
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
423
|
+
let next = original;
|
|
424
|
+
for (const [begin, end] of [
|
|
425
|
+
[POD_BEGIN, POD_END], [LEGACY_POD_BEGIN, LEGACY_POD_END], [LEGACY_BEGIN, LEGACY_END],
|
|
426
|
+
['# COCOS_HYBRID_SAMPLE_BEGIN', '# COCOS_HYBRID_SAMPLE_END'],
|
|
427
|
+
]) next = next.replace(markedPattern(begin, end), '');
|
|
428
|
+
if (/^\s*pod\s+['"](?:FTCocosBridge|HybridSampleHost|GuanceSDK)(?:\/[^'"]*)?['"]/m.test(next)) {
|
|
429
|
+
throw new Error(`[cocos-sdk] Remove the manually declared native SDK Pod from ${file} before switching to SPM.`);
|
|
430
|
+
}
|
|
431
|
+
if (next !== original) fs.writeFileSync(file, next);
|
|
432
|
+
const directory = path.dirname(file);
|
|
433
|
+
const existingLock = path.join(directory, 'Podfile.lock');
|
|
434
|
+
const hasSDKPods = fs.existsSync(existingLock)
|
|
435
|
+
&& /^\s*- (?:FTCocosBridge|HybridSampleHost|GuanceSDK)(?:\/|\s|:)/m.test(fs.readFileSync(existingLock, 'utf8'));
|
|
436
|
+
if (next === original && !hasSDKPods) continue;
|
|
437
|
+
if (fs.existsSync(path.join(directory, 'Podfile.lock')) || fs.existsSync(path.join(directory, 'Pods'))) {
|
|
438
|
+
logger.info('[cocos-sdk] Updating existing Pods integration for the SPM switch.');
|
|
439
|
+
try {
|
|
440
|
+
execFileSync('pod', ['install'], { cwd: directory, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
|
|
441
|
+
} catch (error) {
|
|
442
|
+
fs.writeFileSync(file, original);
|
|
443
|
+
throw new Error(`[cocos-sdk] Unable to update existing Pods in ${directory}: ${error.message}`);
|
|
444
|
+
}
|
|
445
|
+
const lock = path.join(directory, 'Podfile.lock');
|
|
446
|
+
if (fs.existsSync(lock) && /^\s*- GuanceSDK(?:\/|\s|:)/m.test(fs.readFileSync(lock, 'utf8'))) {
|
|
447
|
+
throw new Error(`[cocos-sdk] Another Pod still depends on the native SDK in ${lock}; migrate that dependency before using SPM.`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
module.exports = { installNative, readIosDependencyManager };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { createHash } = require('crypto');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function readProject(file) {
|
|
9
|
+
// plutil understands both Xcode's OpenStep format and XML plists, without Ruby/gems.
|
|
10
|
+
return JSON.parse(execFileSync('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file], {
|
|
11
|
+
encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeProject(file, project) {
|
|
16
|
+
const next = `// !$*UTF8*$!\n// COCOS_SDK_XCODE_PROJECT\n${serialize(project)}\n`;
|
|
17
|
+
if (fs.readFileSync(file, 'utf8') !== next) fs.writeFileSync(file, next);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function serialize(value, depth = 0) {
|
|
21
|
+
const indent = '\t'.repeat(depth);
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return `(\n${value.map((entry) => `${indent}\t${serialize(entry, depth + 1)},\n`).join('')}${indent})`;
|
|
24
|
+
}
|
|
25
|
+
if (value && typeof value === 'object') {
|
|
26
|
+
return `{\n${Object.entries(value).sort(([a], [b]) => a === 'isa' ? -1 : b === 'isa' ? 1 : a.localeCompare(b)).map(([key, entry]) => (
|
|
27
|
+
`${indent}\t${serialize(key)} = ${serialize(entry, depth + 1)};\n`
|
|
28
|
+
)).join('')}${indent}}`;
|
|
29
|
+
}
|
|
30
|
+
const text = String(value);
|
|
31
|
+
return /^[A-Za-z0-9_./]+$/.test(text) ? text : JSON.stringify(text);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applicationTarget(project, preferredName) {
|
|
35
|
+
const targets = Object.entries(project.objects).filter(([, object]) => (
|
|
36
|
+
object.isa === 'PBXNativeTarget' && object.productType === 'com.apple.product-type.application'
|
|
37
|
+
));
|
|
38
|
+
return targets.find(([, object]) => object.name === preferredName)
|
|
39
|
+
|| targets.find(([, object]) => /(?:^|[-_.])(?:mobile|ios)$/i.test(object.name))
|
|
40
|
+
|| targets[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function add(list, value) {
|
|
44
|
+
if (!list.includes(value)) list.push(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function identifier(value) {
|
|
48
|
+
return createHash('sha256').update(`cocos-sdk:${value}`).digest('hex').slice(0, 24).toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function installSwiftPackage(projectFile, packageDirectory, productName, targetName) {
|
|
52
|
+
if (!fs.existsSync(path.join(packageDirectory, 'Package.swift'))) {
|
|
53
|
+
throw new Error(`[cocos-sdk] Missing Swift package: ${packageDirectory}`);
|
|
54
|
+
}
|
|
55
|
+
const project = readProject(projectFile);
|
|
56
|
+
const selected = applicationTarget(project, targetName);
|
|
57
|
+
if (!selected) throw new Error(`[cocos-sdk] No application target in ${projectFile}`);
|
|
58
|
+
const [targetID, target] = selected;
|
|
59
|
+
const objects = project.objects;
|
|
60
|
+
const root = objects[project.rootObject];
|
|
61
|
+
const projectDirectory = path.resolve(path.dirname(path.dirname(projectFile)), root.projectDirPath || '.');
|
|
62
|
+
const relativePath = path.relative(projectDirectory, packageDirectory).split(path.sep).join('/');
|
|
63
|
+
const packageID = identifier(`package:${productName}`);
|
|
64
|
+
const productID = identifier(`product:${targetID}:${productName}`);
|
|
65
|
+
const buildID = identifier(`framework:${targetID}:${productName}`);
|
|
66
|
+
const existing = (target.packageProductDependencies || []).find((id) => (
|
|
67
|
+
id !== productID && objects[id]?.productName === productName
|
|
68
|
+
));
|
|
69
|
+
if (existing) throw new Error(`[cocos-sdk] ${productName} is already managed manually in ${target.name}; remove that reference before automatic installation.`);
|
|
70
|
+
|
|
71
|
+
objects[packageID] = { isa: 'XCLocalSwiftPackageReference', relativePath };
|
|
72
|
+
objects[productID] = { isa: 'XCSwiftPackageProductDependency', productName };
|
|
73
|
+
objects[buildID] = { isa: 'PBXBuildFile', productRef: productID };
|
|
74
|
+
add(root.packageReferences ||= [], packageID);
|
|
75
|
+
add(target.packageProductDependencies ||= [], productID);
|
|
76
|
+
let phaseID = (target.buildPhases || []).find((id) => objects[id]?.isa === 'PBXFrameworksBuildPhase');
|
|
77
|
+
if (!phaseID) {
|
|
78
|
+
phaseID = identifier(`frameworks:${targetID}`);
|
|
79
|
+
objects[phaseID] = { isa: 'PBXFrameworksBuildPhase', buildActionMask: '2147483647', files: [], runOnlyForDeploymentPostprocessing: '0' };
|
|
80
|
+
add(target.buildPhases ||= [], phaseID);
|
|
81
|
+
}
|
|
82
|
+
add(objects[phaseID].files ||= [], buildID);
|
|
83
|
+
const configurations = objects[target.buildConfigurationList]?.buildConfigurations || [];
|
|
84
|
+
for (const configID of configurations) {
|
|
85
|
+
const settings = objects[configID].buildSettings ||= {};
|
|
86
|
+
const flags = settings.OTHER_LDFLAGS;
|
|
87
|
+
if (Array.isArray(flags)) {
|
|
88
|
+
add(flags, '$(inherited)');
|
|
89
|
+
add(flags, '-ObjC');
|
|
90
|
+
} else {
|
|
91
|
+
const parts = [flags || '$(inherited)'];
|
|
92
|
+
if (!String(flags || '').includes('$(inherited)')) add(parts, '$(inherited)');
|
|
93
|
+
if (!/(?:^|\s)-ObjC(?:\s|$)/.test(flags || '')) add(parts, '-ObjC');
|
|
94
|
+
settings.OTHER_LDFLAGS = parts.join(' ');
|
|
95
|
+
}
|
|
96
|
+
if (Number(settings.IPHONEOS_DEPLOYMENT_TARGET || 0) < 12) settings.IPHONEOS_DEPLOYMENT_TARGET = '12.0';
|
|
97
|
+
settings.CLANG_ENABLE_MODULES = 'YES';
|
|
98
|
+
}
|
|
99
|
+
preserveCmakeIntegration(projectFile, project, { packageDirectory, productName, targetName: target.name });
|
|
100
|
+
writeProject(projectFile, project);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const RESTORE_BEGIN = '# COCOS_SDK_SPM_RESTORE_BEGIN';
|
|
104
|
+
const RESTORE_END = '# COCOS_SDK_SPM_RESTORE_END';
|
|
105
|
+
const restorePattern = /\n?# COCOS_SDK_SPM_RESTORE_BEGIN[\s\S]*?# COCOS_SDK_SPM_RESTORE_END\n?/g;
|
|
106
|
+
|
|
107
|
+
function shellQuote(value) {
|
|
108
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function preserveCmakeIntegration(projectFile, project, entry) {
|
|
112
|
+
const objects = project.objects;
|
|
113
|
+
const zeroCheck = Object.values(objects).find((object) => object.name === 'ZERO_CHECK');
|
|
114
|
+
if (!zeroCheck) return;
|
|
115
|
+
// CMake's SYMROOT/OBJROOT overrides select Xcode's legacy build locations,
|
|
116
|
+
// which reject packages. Keep CONFIGURATION_BUILD_DIR (engine archives).
|
|
117
|
+
const stateFile = path.join(path.dirname(projectFile), 'cocos-sdk-spm.json');
|
|
118
|
+
const state = fs.existsSync(stateFile)
|
|
119
|
+
? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : { packages: [], buildLocations: {} };
|
|
120
|
+
for (const [id, object] of Object.entries(objects)) {
|
|
121
|
+
if (object.isa !== 'XCBuildConfiguration') continue;
|
|
122
|
+
for (const key of ['SYMROOT', 'OBJROOT']) {
|
|
123
|
+
if (object.buildSettings?.[key] === undefined) continue;
|
|
124
|
+
(state.buildLocations[id] ||= {})[key] = object.buildSettings[key];
|
|
125
|
+
delete object.buildSettings[key];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const relativeDirectory = path.relative(path.dirname(projectFile), entry.packageDirectory);
|
|
129
|
+
state.packages = state.packages.filter((item) => item.productName !== entry.productName);
|
|
130
|
+
state.packages.push({ ...entry, packageDirectory: relativeDirectory });
|
|
131
|
+
state.packages.sort((a, b) => a.productName.localeCompare(b.productName));
|
|
132
|
+
const helper = path.join(path.dirname(projectFile), 'cocos-sdk-spm.cjs');
|
|
133
|
+
if (path.resolve(helper) !== path.resolve(__filename)) fs.copyFileSync(__filename, helper);
|
|
134
|
+
const restore = [
|
|
135
|
+
RESTORE_BEGIN,
|
|
136
|
+
`${process.versions.electron ? 'ELECTRON_RUN_AS_NODE=1 ' : ''}${shellQuote(process.execPath)} ${shellQuote(helper)} --restore`,
|
|
137
|
+
RESTORE_END,
|
|
138
|
+
].join('\n');
|
|
139
|
+
for (const id of zeroCheck.buildPhases || []) {
|
|
140
|
+
const phase = objects[id];
|
|
141
|
+
if (phase?.isa !== 'PBXShellScriptBuildPhase') continue;
|
|
142
|
+
phase.shellScript = `${(phase.shellScript || '').replace(restorePattern, '').trimEnd()}\n${restore}\n`;
|
|
143
|
+
}
|
|
144
|
+
const serialized = `${JSON.stringify(state, null, 2)}\n`;
|
|
145
|
+
if (!fs.existsSync(stateFile) || fs.readFileSync(stateFile, 'utf8') !== serialized) fs.writeFileSync(stateFile, serialized);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function removeSwiftPackages(projectFile) {
|
|
149
|
+
const project = readProject(projectFile);
|
|
150
|
+
const objects = project.objects;
|
|
151
|
+
const removed = new Set();
|
|
152
|
+
for (const productName of ['FTCocosBridge', 'HybridSampleHost']) {
|
|
153
|
+
const packageID = identifier(`package:${productName}`);
|
|
154
|
+
if (!objects[packageID]) continue;
|
|
155
|
+
removed.add(packageID);
|
|
156
|
+
for (const [id, object] of Object.entries(objects)) {
|
|
157
|
+
if (object.isa !== 'PBXNativeTarget') continue;
|
|
158
|
+
removed.add(identifier(`product:${id}:${productName}`));
|
|
159
|
+
removed.add(identifier(`framework:${id}:${productName}`));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!removed.size) return;
|
|
163
|
+
for (const id of removed) delete objects[id];
|
|
164
|
+
for (const object of Object.values(objects)) {
|
|
165
|
+
for (const key of ['packageReferences', 'packageProductDependencies', 'files']) {
|
|
166
|
+
if (Array.isArray(object[key])) object[key] = object[key].filter((id) => !removed.has(id));
|
|
167
|
+
}
|
|
168
|
+
if (object.isa === 'PBXShellScriptBuildPhase') object.shellScript = (object.shellScript || '').replace(restorePattern, '');
|
|
169
|
+
}
|
|
170
|
+
const stateFile = path.join(path.dirname(projectFile), 'cocos-sdk-spm.json');
|
|
171
|
+
if (fs.existsSync(stateFile)) {
|
|
172
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
173
|
+
for (const [id, settings] of Object.entries(state.buildLocations)) {
|
|
174
|
+
if (objects[id]) Object.assign(objects[id].buildSettings ||= {}, settings);
|
|
175
|
+
}
|
|
176
|
+
fs.unlinkSync(stateFile);
|
|
177
|
+
fs.rmSync(path.join(path.dirname(projectFile), 'cocos-sdk-spm.cjs'), { force: true });
|
|
178
|
+
}
|
|
179
|
+
writeProject(projectFile, project);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { installSwiftPackage, removeSwiftPackages, readProject, applicationTarget };
|
|
183
|
+
|
|
184
|
+
if (require.main === module && process.argv[2] === '--restore') {
|
|
185
|
+
const directory = path.dirname(__filename);
|
|
186
|
+
const state = JSON.parse(fs.readFileSync(path.join(directory, 'cocos-sdk-spm.json'), 'utf8'));
|
|
187
|
+
for (const item of state.packages) {
|
|
188
|
+
installSwiftPackage(path.join(directory, 'project.pbxproj'), path.resolve(directory, item.packageDirectory), item.productName, item.targetName);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -124,6 +124,10 @@ public final class FTCocosBridge {
|
|
|
124
124
|
|
|
125
125
|
private static Object dispatch(String method, JSONObject payload) throws Exception {
|
|
126
126
|
switch (method) {
|
|
127
|
+
case "replay.beginSaveImage":
|
|
128
|
+
return FTCocosReplayImageJobs.begin(payload);
|
|
129
|
+
case "replay.pollSaveImage":
|
|
130
|
+
return FTCocosReplayImageJobs.poll(payload.getString("job"));
|
|
127
131
|
case "hybrid.attach":
|
|
128
132
|
attachHybrid(payload);
|
|
129
133
|
return null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.ft.sdk.cocos;
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject;
|
|
4
|
+
import java.util.UUID;
|
|
5
|
+
import java.util.concurrent.ExecutorService;
|
|
6
|
+
import java.util.concurrent.Executors;
|
|
7
|
+
|
|
8
|
+
/** One image in flight across JS runtimes; the caller polls without waiting on disk/encoding. */
|
|
9
|
+
final class FTCocosReplayImageJobs {
|
|
10
|
+
private static final ExecutorService worker = Executors.newSingleThreadExecutor(runnable -> {
|
|
11
|
+
Thread thread = new Thread(runnable, "cocos-replay-image");
|
|
12
|
+
thread.setDaemon(true);
|
|
13
|
+
return thread;
|
|
14
|
+
});
|
|
15
|
+
private static Job current;
|
|
16
|
+
|
|
17
|
+
private static final class Job {
|
|
18
|
+
final String id = UUID.randomUUID().toString();
|
|
19
|
+
String response;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static synchronized String begin(JSONObject arguments) throws Exception {
|
|
23
|
+
final String method = arguments.getString("method");
|
|
24
|
+
if (!"replay.saveImage".equals(method) && !"replay.saveImageV2".equals(method)) {
|
|
25
|
+
throw new IllegalArgumentException("Unsupported asynchronous Replay method");
|
|
26
|
+
}
|
|
27
|
+
final String payload = arguments.getJSONObject("arguments").toString();
|
|
28
|
+
if (current != null && current.response == null) throw new IllegalStateException("Replay encoder is busy");
|
|
29
|
+
final Job job = new Job();
|
|
30
|
+
current = job;
|
|
31
|
+
try {
|
|
32
|
+
worker.execute(() -> {
|
|
33
|
+
String response = FTCocosBridge.invokeLocal(method, payload);
|
|
34
|
+
synchronized (FTCocosReplayImageJobs.class) {
|
|
35
|
+
job.response = response;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} catch (RuntimeException error) {
|
|
39
|
+
current = null;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
return job.id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static synchronized JSONObject poll(String id) throws Exception {
|
|
46
|
+
if (current == null || !current.id.equals(id)) throw new IllegalArgumentException("Replay encoding job is unavailable");
|
|
47
|
+
JSONObject result = new JSONObject();
|
|
48
|
+
result.put("pending", current.response == null);
|
|
49
|
+
if (current.response != null) {
|
|
50
|
+
result.put("response", current.response);
|
|
51
|
+
current = null;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
#import "FTCocosBridge.h"
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
#import "FTCocosReplayImageJobs.h"
|
|
3
|
+
|
|
4
|
+
#import "FTMobileAgent.h"
|
|
5
|
+
#import "FTSDKConfig.h"
|
|
6
|
+
#import "FTRumConfig.h"
|
|
7
|
+
#import "FTLoggerConfig.h"
|
|
8
|
+
#import "FTMobileConfig.h"
|
|
9
|
+
#import "FTExternalDataManager.h"
|
|
10
|
+
#import "FTResourceMetricsModel.h"
|
|
11
|
+
#import "FTResourceContentModel.h"
|
|
4
12
|
#import "FTSessionReplay.h"
|
|
5
13
|
#import <UIKit/UIKit.h>
|
|
6
14
|
#import <objc/message.h>
|
|
@@ -24,6 +32,10 @@
|
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
+ (id)dispatch:(NSString *)method arguments:(NSDictionary *)arguments {
|
|
35
|
+
if ([method isEqualToString:@"replay.beginSaveImage"]) return [self beginSaveImage:arguments];
|
|
36
|
+
if ([method isEqualToString:@"replay.pollSaveImage"]) {
|
|
37
|
+
return [FTCocosReplayImageJobs poll:[self requiredText:arguments key:@"job"]];
|
|
38
|
+
}
|
|
27
39
|
FTExternalDataManager *rum = [FTExternalDataManager sharedManager];
|
|
28
40
|
if ([method isEqualToString:@"hybrid.attach"]) {
|
|
29
41
|
[self ensureNativeHostInitialized];
|
|
@@ -254,6 +266,18 @@
|
|
|
254
266
|
return nil;
|
|
255
267
|
}
|
|
256
268
|
|
|
269
|
+
+ (NSString *)beginSaveImage:(NSDictionary *)arguments {
|
|
270
|
+
NSString *method = [self requiredText:arguments key:@"method"];
|
|
271
|
+
if (![method isEqualToString:@"replay.saveImage"] && ![method isEqualToString:@"replay.saveImageV2"]) {
|
|
272
|
+
[self fail:@"Unsupported asynchronous Replay method"];
|
|
273
|
+
}
|
|
274
|
+
NSDictionary *payload = [[self dictionary:arguments[@"arguments"]] copy];
|
|
275
|
+
if (!payload) [self fail:@"Replay image arguments are required"];
|
|
276
|
+
return [FTCocosReplayImageJobs beginWithWork:^NSString *{
|
|
277
|
+
return [self responseWithValue:[self dispatch:method arguments:payload] error:nil];
|
|
278
|
+
}];
|
|
279
|
+
}
|
|
280
|
+
|
|
257
281
|
+ (void)ensureNativeHostInitialized {
|
|
258
282
|
@try {
|
|
259
283
|
if (![FTMobileAgent sharedInstance]) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Pod::Spec.new do |s|
|
|
2
2
|
s.name = 'FTCocosBridge'
|
|
3
|
-
s.version = '0.1.0-alpha.
|
|
3
|
+
s.version = '0.1.0-alpha.5'
|
|
4
4
|
s.summary = 'CloudCare Mobile SDK native bridge for Cocos Creator.'
|
|
5
5
|
s.homepage = 'https://github.com/GuanceCloud/datakit-cocos'
|
|
6
6
|
s.license = { :type => 'Apache-2.0' }
|
|
7
7
|
s.author = { 'Guance Cloud' => 'support@guance.com' }
|
|
8
8
|
s.source = { :path => '.' }
|
|
9
9
|
s.platform = :ios, '12.0'
|
|
10
|
-
s.source_files = 'FTCocosBridge.{h,m}'
|
|
10
|
+
s.source_files = 'FTCocosBridge.{h,m}', 'FTCocosReplayImageJobs.{h,m}'
|
|
11
11
|
s.public_header_files = 'FTCocosBridge.h'
|
|
12
12
|
s.requires_arc = true
|
|
13
13
|
# Cocos resolves FTCocosBridge by class name at runtime, so no direct symbol
|
|
14
14
|
# reference would otherwise keep the Objective-C category/class in the app.
|
|
15
15
|
s.user_target_xcconfig = { 'OTHER_LDFLAGS' => '$(inherited) -ObjC' }
|
|
16
|
-
s.dependency 'GuanceSDK/Agent', '1.6.8-alpha.
|
|
17
|
-
s.dependency 'GuanceSDK/FTSessionReplay', '1.6.8-alpha.
|
|
16
|
+
s.dependency 'GuanceSDK/Agent', '1.6.8-alpha.5'
|
|
17
|
+
s.dependency 'GuanceSDK/FTSessionReplay', '1.6.8-alpha.5'
|
|
18
18
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/** One background image operation, with nonblocking result retrieval from JSB. */
|
|
6
|
+
@interface FTCocosReplayImageJobs : NSObject
|
|
7
|
+
+ (NSString *)beginWithWork:(NSString *(^)(void))work;
|
|
8
|
+
+ (NSDictionary *)poll:(NSString *)identifier;
|
|
9
|
+
@end
|
|
10
|
+
|
|
11
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#import "FTCocosReplayImageJobs.h"
|
|
2
|
+
|
|
3
|
+
@interface FTCocosReplayImageJob : NSObject
|
|
4
|
+
@property (nonatomic, copy) NSString *identifier;
|
|
5
|
+
@property (nonatomic, copy) NSString *response;
|
|
6
|
+
@end
|
|
7
|
+
@implementation FTCocosReplayImageJob
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
static FTCocosReplayImageJob *currentJob;
|
|
11
|
+
|
|
12
|
+
@implementation FTCocosReplayImageJobs
|
|
13
|
+
|
|
14
|
+
+ (NSString *)beginWithWork:(NSString *(^)(void))work {
|
|
15
|
+
FTCocosReplayImageJob *job = [FTCocosReplayImageJob new];
|
|
16
|
+
job.identifier = NSUUID.UUID.UUIDString;
|
|
17
|
+
@synchronized (self) {
|
|
18
|
+
if (currentJob && !currentJob.response) {
|
|
19
|
+
[NSException raise:NSInternalInconsistencyException format:@"Replay encoder is busy"];
|
|
20
|
+
}
|
|
21
|
+
currentJob = job;
|
|
22
|
+
}
|
|
23
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
24
|
+
@autoreleasepool {
|
|
25
|
+
NSString *response;
|
|
26
|
+
@try {
|
|
27
|
+
response = work();
|
|
28
|
+
} @catch (NSException *exception) {
|
|
29
|
+
NSData *data = [NSJSONSerialization dataWithJSONObject:@{
|
|
30
|
+
@"ok": @NO, @"error": exception.reason ?: @"Replay encoding failed"
|
|
31
|
+
} options:0 error:nil];
|
|
32
|
+
response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
33
|
+
}
|
|
34
|
+
@synchronized (self) {
|
|
35
|
+
job.response = response ?: @"{\"ok\":false,\"error\":\"Empty Replay encoding result\"}";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return job.identifier;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
+ (NSDictionary *)poll:(NSString *)identifier {
|
|
43
|
+
@synchronized (self) {
|
|
44
|
+
if (![currentJob.identifier isEqualToString:identifier]) {
|
|
45
|
+
[NSException raise:NSInvalidArgumentException format:@"Replay encoding job is unavailable"];
|
|
46
|
+
}
|
|
47
|
+
if (!currentJob.response) return @{ @"pending": @YES };
|
|
48
|
+
NSDictionary *result = @{ @"pending": @NO, @"response": currentJob.response };
|
|
49
|
+
currentJob = nil;
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// swift-tools-version:5.3
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "FTCocosBridge",
|
|
6
|
+
platforms: [.iOS(.v12)],
|
|
7
|
+
products: [.library(name: "FTCocosBridge", targets: ["FTCocosBridge"])],
|
|
8
|
+
dependencies: [
|
|
9
|
+
.package(url: "https://github.com/GuanceCloud/datakit-ios.git", .exact("1.6.8-alpha.5")),
|
|
10
|
+
],
|
|
11
|
+
targets: [
|
|
12
|
+
.target(
|
|
13
|
+
name: "FTCocosBridge",
|
|
14
|
+
dependencies: [
|
|
15
|
+
.product(name: "GuanceSDK", package: "datakit-ios"),
|
|
16
|
+
.product(name: "GuanceSessionReplay", package: "datakit-ios"),
|
|
17
|
+
],
|
|
18
|
+
path: ".",
|
|
19
|
+
exclude: ["FTCocosBridge.podspec"],
|
|
20
|
+
sources: ["FTCocosBridge.m", "FTCocosReplayImageJobs.m"],
|
|
21
|
+
publicHeadersPath: "include",
|
|
22
|
+
cSettings: [.headerSearchPath(".")],
|
|
23
|
+
linkerSettings: [.linkedFramework("UIKit")]
|
|
24
|
+
),
|
|
25
|
+
]
|
|
26
|
+
)
|