microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.11.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/ios/plistBuddy.ts

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