microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.7.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/ios/plistBuddy.ts

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