microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.4.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/ios/plistBuddy.ts

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