microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
test-microbuild1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/ios/plistBuddy.ts

355lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as path from "path";
5import * as fs from "fs";
6import * as glob from "glob";
7import * as semver from "semver";
8import { Node } from "../../common/node/node";
9import { ChildProcess } from "../../common/node/childProcess";
10import { ErrorHelper } from "../../common/error/errorHelper";
11import { InternalErrorCode } from "../../common/error/internalErrorCode";
12import { ProjectVersionHelper } from "../../common/projectVersionHelper";
13import { getFileNameWithoutExtension } from "../../common/utils";
14import customRequire from "../../common/customRequire";
15import { PlatformType } from "../launchArgs";
16import { AppLauncher } from "../appLauncher";
17import { SettingsHelper } from "../settingsHelper";
18
19export interface ConfigurationData {
20 fullProductName: string;
21 configurationFolder: string;
22}
23export interface IOSBuildLocationData {
24 executable: string;
25 configurationFolder: string;
26}
27export class PlistBuddy {
28 private static readonly plistBuddyExecutable = "/usr/libexec/PlistBuddy";
29 private static readonly SCHEME_IN_PRODUCTS_FOLDER_PATH_VERSION = "0.59.0";
30 private static readonly NEW_RN_IOS_CLI_LOCATION_VERSION = "0.60.0";
31 private static readonly RN69_FUND_XCODE_PROJECT_LOCATION_VERSION = "0.69.0";
32 private static readonly RN_VERSION_CLI_PLATFORM_APPLE = "0.74.0";
33 private static readonly RN_VERSION_CLI_CONFIG_APPLE = "0.76.2";
34 private readonly TARGET_BUILD_DIR_SEARCH_KEY = "TARGET_BUILD_DIR";
35 private readonly FULL_PRODUCT_NAME_SEARCH_KEY = "FULL_PRODUCT_NAME";
36 private nodeChildProcess: ChildProcess;
37 constructor({ nodeChildProcess = new Node.ChildProcess() } = {}) {
38 this.nodeChildProcess = nodeChildProcess;
39 }
40 public async getBundleId(
41 platformProjectRoot: string,
42 projectRoot: string,
43 platform: PlatformType.iOS | PlatformType.macOS,
44 simulator: boolean = true,
45 configuration: string = "Debug",
46 productName?: string,
47 scheme?: string,
48 ): Promise<string> {
49 const iOSBuildLocationData = await this.getExecutableAndConfigurationFolder(
50 platformProjectRoot,
51 projectRoot,
52 platform,
53 simulator,
54 configuration,
55 productName,
56 scheme,
57 );
58 const infoPlistPath = path.join(
59 iOSBuildLocationData.configurationFolder,
60 iOSBuildLocationData.executable,
61 platform === PlatformType.iOS ? "Info.plist" : path.join("Contents", "Info.plist"),
62 );
63 return await this.invokePlistBuddy("Print:CFBundleIdentifier", infoPlistPath);
64 }
65 public async getExecutableAndConfigurationFolder(
66 platformProjectRoot: string,
67 projectRoot: string,
68 platform: PlatformType.iOS | PlatformType.macOS,
69 simulator: boolean = true,
70 configuration: string = "Debug",
71 productName?: string,
72 scheme?: string,
73 ): Promise<IOSBuildLocationData> {
74 const rnVersions = await ProjectVersionHelper.getReactNativeVersions(projectRoot);
75 let productsFolder;
76 if (
77 semver.gte(
78 rnVersions.reactNativeVersion,
79 PlistBuddy.SCHEME_IN_PRODUCTS_FOLDER_PATH_VERSION,
80 ) ||
81 ProjectVersionHelper.isCanaryVersion(rnVersions.reactNativeVersion)
82 ) {
83 if (!scheme) {
84 // If no scheme were provided via runOptions.scheme or via runArguments then try to get scheme using the way RN CLI does.
85 scheme = this.getInferredScheme(
86 platformProjectRoot,
87 projectRoot,
88 rnVersions.reactNativeVersion,
89 );
90 if (platform === PlatformType.macOS) {
91 scheme = `${scheme}-macOS`;
92 }
93 }
94 productsFolder = path.join(platformProjectRoot, "build", scheme, "Build", "Products");
95 } else {
96 productsFolder = path.join(platformProjectRoot, "build", "Build", "Products");
97 }
98 const sdkType =
99 platform === PlatformType.iOS ? this.getSdkType(simulator, scheme) : undefined;
100 let configurationFolder = path.join(
101 productsFolder,
102 `${configuration}${sdkType ? `-${sdkType}` : ""}`,
103 );
104 let executable = "";
105 if (productName) {
106 executable = `${productName}.app`;
107 if (!fs.existsSync(path.join(configurationFolder, executable))) {
108 const configurationData = this.getConfigurationData(
109 projectRoot,
110 rnVersions.reactNativeVersion,
111 platformProjectRoot,
112 configuration,
113 scheme,
114 configurationFolder,
115 sdkType,
116 );
117 configurationFolder = configurationData.configurationFolder;
118 }
119 } else {
120 const executableList = this.findExecutable(configurationFolder);
121 if (!executableList.length) {
122 const configurationData_1 = this.getConfigurationData(
123 projectRoot,
124 rnVersions.reactNativeVersion,
125 platformProjectRoot,
126 configuration,
127 scheme,
128 configurationFolder,
129 sdkType,
130 );
131 configurationFolder = configurationData_1.configurationFolder;
132 executableList.push(configurationData_1.fullProductName);
133 } else if (executableList.length > 1) {
134 throw ErrorHelper.getInternalError(
135 InternalErrorCode.IOSFoundMoreThanOneExecutablesCleanupBuildFolder,
136 configurationFolder,
137 );
138 }
139 executable = `${executableList[0]}`;
140 }
141 return {
142 executable,
143 configurationFolder,
144 };
145 }
146 public async setPlistProperty(
147 plistFile: string,
148 property: string,
149 value: string,
150 ): Promise<void> {
151 // Attempt to set the value, and if it fails due to the key not existing attempt to create the key
152 try {
153 await this.invokePlistBuddy(`Set ${property} ${value}`, plistFile);
154 } catch (e) {
155 await this.invokePlistBuddy(`Add ${property} string ${value}`, plistFile);
156 }
157 }
158 public async setPlistBooleanProperty(
159 plistFile: string,
160 property: string,
161 value: boolean,
162 ): Promise<void> {
163 // Attempt to set the value, and if it fails due to the key not existing attempt to create the key
164 try {
165 await this.invokePlistBuddy(`Set ${property} ${String(value)}`, plistFile);
166 } catch (e) {
167 await this.invokePlistBuddy(`Add ${property} bool ${String(value)}`, plistFile);
168 }
169 }
170 public async deletePlistProperty(plistFile: string, property: string): Promise<void> {
171 try {
172 await this.invokePlistBuddy(`Delete ${property}`, plistFile);
173 } catch (err) {
174 if (!err.toString().toLowerCase().includes("does not exist")) {
175 throw err;
176 }
177 }
178 }
179 public readPlistProperty(plistFile: string, property: string): Promise<string> {
180 return this.invokePlistBuddy(`Print ${property}`, plistFile);
181 }
182 public getBuildPathAndProductName(
183 platformProjectRoot: string,
184 projectWorkspaceConfigName: string,
185 configuration: string,
186 scheme: string,
187 sdkType?: string,
188 ): ConfigurationData {
189 const xcodebuildParams = ["-workspace", projectWorkspaceConfigName, "-scheme", scheme];
190 if (sdkType) {
191 xcodebuildParams.push("-sdk", sdkType);
192 }
193 xcodebuildParams.push("-configuration", configuration, "-showBuildSettings");
194 const buildSettings = this.nodeChildProcess.execFileSync("xcodebuild", xcodebuildParams, {
195 encoding: "utf8",
196 cwd: platformProjectRoot,
197 });
198 const targetBuildDir = this.fetchParameterFromBuildSettings(
199 <string>buildSettings,
200 this.TARGET_BUILD_DIR_SEARCH_KEY,
201 );
202 const fullProductName = this.fetchParameterFromBuildSettings(
203 <string>buildSettings,
204 this.FULL_PRODUCT_NAME_SEARCH_KEY,
205 );
206 if (!targetBuildDir) {
207 throw new Error("Failed to get the target build directory.");
208 }
209 if (!fullProductName) {
210 throw new Error("Failed to get full product name.");
211 }
212 return {
213 fullProductName,
214 configurationFolder: targetBuildDir,
215 };
216 }
217 public getInferredScheme(
218 platformProjectRoot: string,
219 projectRoot: string,
220 rnVersion: string,
221 ): string {
222 const projectWorkspaceConfigName = this.getProjectWorkspaceConfigName(
223 platformProjectRoot,
224 projectRoot,
225 rnVersion,
226 );
227 return getFileNameWithoutExtension(projectWorkspaceConfigName);
228 }
229 public getSdkType(simulator: boolean, scheme?: string): string {
230 const sdkSuffix = simulator ? "simulator" : "os";
231 const deviceType =
232 (scheme?.toLowerCase().indexOf("tvos") ?? -1) > -1 ? "appletv" : "iphone";
233 return `${deviceType}${sdkSuffix}`;
234 }
235 public getProjectWorkspaceConfigName(
236 platformProjectRoot: string,
237 projectRoot: string,
238 rnVersion: string,
239 ): string {
240 // Portion of code was taken from https://github.com/react-native-community/cli/blob/master/packages/platform-ios/src/commands/runIOS/index.js
241 // and modified a little bit
242 /**
243 * Copyright (c) Facebook, Inc. and its affiliates.
244 *
245 * This source code is licensed under the MIT license found in the
246 * LICENSE file in the root directory of this source tree.
247 *
248 * @flow
249 * @format
250 */
251
252 const nodeModulesRoot: string = AppLauncher.getNodeModulesRootByProjectPath(projectRoot);
253
254 const iOSCliPlatform = semver.gte(rnVersion, PlistBuddy.RN_VERSION_CLI_PLATFORM_APPLE)
255 ? semver.gte(rnVersion, PlistBuddy.RN_VERSION_CLI_CONFIG_APPLE)
256 ? "cli-config-apple"
257 : "cli-platform-apple"
258 : "cli-platform-ios";
259 const iOSCliFolderName =
260 semver.gte(rnVersion, PlistBuddy.NEW_RN_IOS_CLI_LOCATION_VERSION) ||
261 ProjectVersionHelper.isCanaryVersion(rnVersion)
262 ? iOSCliPlatform
263 : "cli";
264
265 let findXcodeBase = "node_modules/@react-native-community";
266
267 const pnpmProjectPath = path.resolve(nodeModulesRoot, "node_modules", ".pnpm");
268
269 const isPnpmProject =
270 fs.existsSync(pnpmProjectPath) && SettingsHelper.getPackageManager() === "pnpm";
271 if (isPnpmProject) {
272 const modules = fs.readdirSync(pnpmProjectPath);
273 const regex = new RegExp(`\@react-native-community\\+${iOSCliFolderName}@`);
274 const communityModule = modules.find(module => regex.test(module));
275 if (communityModule) {
276 findXcodeBase = path.join(
277 "node_modules",
278 ".pnpm",
279 communityModule,
280 "node_modules",
281 "@react-native-community",
282 );
283 }
284 }
285
286 const findXcodeProjectLocation = `${findXcodeBase}/${iOSCliFolderName}/build/${
287 semver.gte(rnVersion, PlistBuddy.RN69_FUND_XCODE_PROJECT_LOCATION_VERSION)
288 ? "config/findXcodeProject"
289 : "commands/runIOS/findXcodeProject"
290 }`;
291
292 const findXcodeProject = customRequire(
293 path.join(nodeModulesRoot, findXcodeProjectLocation),
294 ).default;
295 const xcodeProject = findXcodeProject(fs.readdirSync(platformProjectRoot));
296 if (!xcodeProject) {
297 throw new Error(
298 `Could not find Xcode project files in "${platformProjectRoot}" folder`,
299 );
300 }
301 return xcodeProject.name;
302 }
303 public getConfigurationData(
304 projectRoot: string,
305 reactNativeVersion: string,
306 platformProjectRoot: string,
307 configuration: string,
308 scheme: string | undefined,
309 oldConfigurationFolder: string,
310 sdkType?: string,
311 ): ConfigurationData {
312 if (!scheme) {
313 throw ErrorHelper.getInternalError(
314 InternalErrorCode.IOSCouldNotFoundExecutableInFolder,
315 oldConfigurationFolder,
316 );
317 }
318 const projectWorkspaceConfigName = this.getProjectWorkspaceConfigName(
319 platformProjectRoot,
320 projectRoot,
321 reactNativeVersion,
322 );
323 return this.getBuildPathAndProductName(
324 platformProjectRoot,
325 projectWorkspaceConfigName,
326 configuration,
327 scheme,
328 sdkType,
329 );
330 }
331 /**
332 * @param {string} buildSettings
333 * @param {string} parameterName
334 * @returns {string | null}
335 */
336 public fetchParameterFromBuildSettings(
337 buildSettings: string,
338 parameterName: string,
339 ): string | null {
340 const targetBuildMatch = new RegExp(`${parameterName} = (.+)$`, "m").exec(buildSettings);
341 return targetBuildMatch && targetBuildMatch[1] ? targetBuildMatch[1].trim() : null;
342 }
343 private findExecutable(folder: string): string[] {
344 return glob.sync("*.app", {
345 cwd: folder,
346 });
347 }
348 private async invokePlistBuddy(command: string, plistFile: string): Promise<string> {
349 const res = await this.nodeChildProcess.exec(
350 `${PlistBuddy.plistBuddyExecutable} -c '${command}' '${plistFile}'`,
351 );
352 const outcome = await res.outcome;
353 return outcome.toString().trim();
354 }
355}
356