Skip to content

Commit 93ae62e

Browse files
authored
refactor(apple): port entitlements.rb to JS (#2420)
1 parent 20e8b43 commit 93ae62e

File tree

6 files changed

+262
-34
lines changed

6 files changed

+262
-34
lines changed

ios/entitlements.mjs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// @ts-check
2+
import * as nodefs from "node:fs";
3+
import * as path from "node:path";
4+
import { isObject, toPlist } from "./utils.mjs";
5+
6+
/** @import { ApplePlatform, JSONObject } from "../scripts/types.js"; */
7+
8+
const DEFAULT_IOS_ENTITLEMENTS = {
9+
"keychain-access-groups": ["$(AppIdentifierPrefix)com.microsoft.adalcache"],
10+
};
11+
12+
const DEFAULT_MACOS_ENTITLEMENTS = {
13+
"com.apple.security.app-sandbox": true,
14+
"com.apple.security.files.user-selected.read-only": true,
15+
"com.apple.security.network.client": true,
16+
};
17+
18+
/**
19+
* @param {JSONObject} appConfig
20+
* @param {ApplePlatform} targetPlatform
21+
* @returns {string | JSONObject | undefined}
22+
*/
23+
function getCodeSignEntitlements(appConfig, targetPlatform) {
24+
const platformConfig = appConfig[targetPlatform];
25+
if (!isObject(platformConfig)) {
26+
return;
27+
}
28+
29+
const codeSignEntitlements = platformConfig["codeSignEntitlements"];
30+
if (
31+
typeof codeSignEntitlements !== "string" &&
32+
!isObject(codeSignEntitlements)
33+
) {
34+
return;
35+
}
36+
37+
return codeSignEntitlements;
38+
}
39+
40+
/**
41+
* @param {JSONObject} appConfig
42+
* @param {ApplePlatform} targetPlatform
43+
* @param {string} destination
44+
* @returns {void}
45+
*/
46+
export function generateEntitlements(
47+
appConfig,
48+
targetPlatform,
49+
destination,
50+
fs = nodefs
51+
) {
52+
// If `codeSignEntitlements` is a string, set `CODE_SIGN_ENTITLEMENTS` instead
53+
const userEntitlements = getCodeSignEntitlements(appConfig, targetPlatform);
54+
if (typeof userEntitlements === "string") {
55+
return;
56+
}
57+
58+
const filename = "App.entitlements";
59+
const entitlements = toPlist(
60+
{
61+
...(targetPlatform === "macos"
62+
? DEFAULT_MACOS_ENTITLEMENTS
63+
: DEFAULT_IOS_ENTITLEMENTS),
64+
...userEntitlements,
65+
},
66+
filename
67+
);
68+
69+
fs.writeFileSync(path.join(destination, filename), entitlements);
70+
}

ios/privacyManifest.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ function getUserPrivacyManifest(appConfig, targetPlatform) {
5454
* @param {JSONObject} appConfig
5555
* @param {ApplePlatform} targetPlatform
5656
* @param {string} destination
57-
* @returns {Promise<void>}
57+
* @returns {void}
5858
*/
59-
export async function generatePrivacyManifest(
59+
export function generatePrivacyManifest(
6060
appConfig,
6161
targetPlatform,
6262
destination,
@@ -106,6 +106,7 @@ export async function generatePrivacyManifest(
106106
}
107107
}
108108

109-
const xcprivacy = await toPlist(manifest);
110-
fs.writeFileSync(path.join(destination, "PrivacyInfo.xcprivacy"), xcprivacy);
109+
const filename = "PrivacyInfo.xcprivacy";
110+
const xcprivacy = toPlist(manifest, filename);
111+
fs.writeFileSync(path.join(destination, filename), xcprivacy);
111112
}

ios/utils.mjs

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @ts-check
2-
import { spawn } from "node:child_process";
2+
import { spawnSync } from "node:child_process";
33
import * as path from "node:path";
44
import { fileURLToPath } from "node:url";
55

@@ -29,28 +29,19 @@ export function projectPath(p, targetPlatform) {
2929

3030
/**
3131
* @param {JSONObject} source
32-
* @returns {Promise<string>}
32+
* @param {string} filename
33+
* @returns {string}
3334
*/
34-
export function toPlist(source) {
35-
return new Promise((resolve, reject) => {
36-
const args = ["-convert", "xml1", "-r", "-o", "-", "--", "-"];
37-
const plutil = spawn("/usr/bin/plutil", args, {
38-
stdio: ["pipe", "pipe", "inherit"],
39-
});
40-
41-
/** @type {string[]} */
42-
const data = [];
43-
plutil.stdout.on("data", (chunk) => data.push(chunk.toString()));
35+
export function toPlist(source, filename) {
36+
const args = ["-convert", "xml1", "-r", "-o", "-", "--", "-"];
37+
const plutil = spawnSync("/usr/bin/plutil", args, {
38+
stdio: ["pipe", "pipe", "inherit"],
39+
input: JSON.stringify(source),
40+
});
4441

45-
plutil.on("exit", (exitCode) => {
46-
if (exitCode !== 0) {
47-
reject(new Error("Failed to generate 'PrivacyInfo.xcprivacy'"));
48-
} else {
49-
resolve(data.join(""));
50-
}
51-
});
42+
if (plutil.status !== 0) {
43+
throw new Error(`Failed to generate '${filename}'`);
44+
}
5245

53-
plutil.stdin.write(JSON.stringify(source));
54-
plutil.stdin.end();
55-
});
46+
return plutil.stdout.toString();
5647
}

test/ios/entitlements.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { deepEqual, throws } from "node:assert/strict";
2+
import { afterEach, describe, it } from "node:test";
3+
import { generateEntitlements as generateEntitlementsActual } from "../../ios/entitlements.mjs";
4+
import type { JSONObject } from "../../scripts/types.ts";
5+
import { fs, setMockFiles } from "../fs.mock.ts";
6+
7+
const macosOnly = { skip: process.platform === "win32" };
8+
9+
describe("generatePrivacyManifest()", macosOnly, () => {
10+
const targetPlatforms = ["ios", "macos", "visionos"] as const;
11+
12+
function generateEntitlements(
13+
config: JSONObject,
14+
platform: (typeof targetPlatforms)[number]
15+
): void {
16+
const destination = ".";
17+
fs.mkdirSync(destination, { recursive: true, mode: 0o755 });
18+
generateEntitlementsActual(config, platform, destination, fs);
19+
}
20+
21+
function readEntitlements() {
22+
return fs
23+
.readFileSync("App.entitlements", { encoding: "utf-8" })
24+
.split("\n");
25+
}
26+
27+
afterEach(() => {
28+
setMockFiles();
29+
});
30+
31+
for (const platform of targetPlatforms) {
32+
it(`[${platform}] generates a default manifest`, () => {
33+
generateEntitlements({}, platform);
34+
35+
deepEqual(readEntitlements(), DEFAULT_ENTITLEMENTS[platform]);
36+
});
37+
38+
it(`[${platform}] handles invalid manifest`, () => {
39+
generateEntitlements(
40+
{ [platform]: { codeSignEntitlements: false } },
41+
platform
42+
);
43+
44+
deepEqual(readEntitlements(), DEFAULT_ENTITLEMENTS[platform]);
45+
});
46+
47+
it(`[${platform}] does not generate a manifest when a path is specified`, () => {
48+
generateEntitlements(
49+
{ [platform]: { codeSignEntitlements: "App.entitlements" } },
50+
platform
51+
);
52+
53+
throws(readEntitlements);
54+
});
55+
56+
it(`[${platform}] appends to default manifest`, () => {
57+
generateEntitlements(
58+
{
59+
[platform]: {
60+
codeSignEntitlements: {
61+
"com.apple.developer.game-center": true,
62+
},
63+
},
64+
},
65+
platform
66+
);
67+
68+
deepEqual(readEntitlements(), APP_ENTITLEMENTS[platform]);
69+
});
70+
}
71+
});
72+
73+
const DEFAULT_ENTITLEMENTS = {
74+
ios: [
75+
'<?xml version="1.0" encoding="UTF-8"?>',
76+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
77+
'<plist version="1.0">',
78+
"<dict>",
79+
" <key>keychain-access-groups</key>",
80+
" <array>",
81+
" <string>$(AppIdentifierPrefix)com.microsoft.adalcache</string>",
82+
" </array>",
83+
"</dict>",
84+
"</plist>",
85+
"",
86+
],
87+
macos: [
88+
'<?xml version="1.0" encoding="UTF-8"?>',
89+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
90+
'<plist version="1.0">',
91+
"<dict>",
92+
" <key>com.apple.security.app-sandbox</key>",
93+
" <true/>",
94+
" <key>com.apple.security.files.user-selected.read-only</key>",
95+
" <true/>",
96+
" <key>com.apple.security.network.client</key>",
97+
" <true/>",
98+
"</dict>",
99+
"</plist>",
100+
"",
101+
],
102+
visionos: [
103+
'<?xml version="1.0" encoding="UTF-8"?>',
104+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
105+
'<plist version="1.0">',
106+
"<dict>",
107+
" <key>keychain-access-groups</key>",
108+
" <array>",
109+
" <string>$(AppIdentifierPrefix)com.microsoft.adalcache</string>",
110+
" </array>",
111+
"</dict>",
112+
"</plist>",
113+
"",
114+
],
115+
};
116+
117+
const APP_ENTITLEMENTS = {
118+
ios: [
119+
'<?xml version="1.0" encoding="UTF-8"?>',
120+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
121+
'<plist version="1.0">',
122+
"<dict>",
123+
" <key>com.apple.developer.game-center</key>",
124+
" <true/>",
125+
" <key>keychain-access-groups</key>",
126+
" <array>",
127+
" <string>$(AppIdentifierPrefix)com.microsoft.adalcache</string>",
128+
" </array>",
129+
"</dict>",
130+
"</plist>",
131+
"",
132+
],
133+
macos: [
134+
'<?xml version="1.0" encoding="UTF-8"?>',
135+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
136+
'<plist version="1.0">',
137+
"<dict>",
138+
" <key>com.apple.developer.game-center</key>",
139+
" <true/>",
140+
" <key>com.apple.security.app-sandbox</key>",
141+
" <true/>",
142+
" <key>com.apple.security.files.user-selected.read-only</key>",
143+
" <true/>",
144+
" <key>com.apple.security.network.client</key>",
145+
" <true/>",
146+
"</dict>",
147+
"</plist>",
148+
"",
149+
],
150+
visionos: [
151+
'<?xml version="1.0" encoding="UTF-8"?>',
152+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
153+
'<plist version="1.0">',
154+
"<dict>",
155+
" <key>com.apple.developer.game-center</key>",
156+
" <true/>",
157+
" <key>keychain-access-groups</key>",
158+
" <array>",
159+
" <string>$(AppIdentifierPrefix)com.microsoft.adalcache</string>",
160+
" </array>",
161+
"</dict>",
162+
"</plist>",
163+
"",
164+
],
165+
};

test/ios/privacyManifest.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { fs, setMockFiles } from "../fs.mock.ts";
77
const macosOnly = { skip: process.platform === "win32" };
88

99
describe("generatePrivacyManifest()", macosOnly, () => {
10-
function generatePrivacyManifest(config: JSONObject): Promise<void> {
10+
function generatePrivacyManifest(config: JSONObject): void {
1111
const destination = ".";
1212
fs.mkdirSync(destination, { recursive: true, mode: 0o755 });
13-
return generatePrivacyManifestActual(config, "ios", destination, fs);
13+
generatePrivacyManifestActual(config, "ios", destination, fs);
1414
}
1515

1616
function readPrivacyManifest() {
@@ -23,20 +23,20 @@ describe("generatePrivacyManifest()", macosOnly, () => {
2323
setMockFiles();
2424
});
2525

26-
it("generates a default manifest", async () => {
27-
await generatePrivacyManifest({});
26+
it("generates a default manifest", () => {
27+
generatePrivacyManifest({});
2828

2929
deepEqual(readPrivacyManifest(), DEFAULT_PRIVACY_MANIFEST);
3030
});
3131

32-
it("handles invalid configuration", async () => {
33-
await generatePrivacyManifest({ ios: { privacyManifest: "YES" } });
32+
it("handles invalid configuration", () => {
33+
generatePrivacyManifest({ ios: { privacyManifest: "YES" } });
3434

3535
deepEqual(readPrivacyManifest(), DEFAULT_PRIVACY_MANIFEST);
3636
});
3737

38-
it("appends to default manifest", async () => {
39-
await generatePrivacyManifest({
38+
it("appends to default manifest", () => {
39+
generatePrivacyManifest({
4040
ios: {
4141
privacyManifest: {
4242
NSPrivacyTracking: true,

test/pack.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ describe("npm pack", () => {
153153
"ios/ReactTestAppUITests/ReactTestAppUITests.swift",
154154
"ios/assetsCatalog.mjs",
155155
"ios/assets_catalog.rb",
156+
"ios/entitlements.mjs",
156157
"ios/entitlements.rb",
157158
"ios/info_plist.rb",
158159
"ios/node.rb",

0 commit comments

Comments
 (0)