microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b7645ff29f00ff7bc3345dffe414869527f83c8c

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/ios/plistBuddy.ts

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