microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.5.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/ios/plistBuddy.ts

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