Skip to content

Commit 47737aa

Browse files
committed
fix(ios): keep pod build settings literal and out of pod-less apps
The build-settings post_install section was written for every app, so apps without pods got a Podfile and ran `pod install` on every prepare. It's now only added when the project already has a Podfile, and removed first so it never keeps one alive on its own. Removing sections no longer leaves a Podfile that only differs from the empty default by blank lines. Values were written as Ruby double-quoted strings, which interpolate `#{}`, `#$` and `#@`. A build.xcconfig value or project path containing them ran as Ruby during `pod install`. They're now single-quoted literals.
1 parent 9fc836b commit 47737aa

2 files changed

Lines changed: 101 additions & 34 deletions

File tree

‎lib/services/cocoapods-service.ts‎

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ import { injector } from "../common/yok";
2323
import { XcodeSelectService } from "../common/services/xcode-select-service";
2424
import * as constants from "../constants";
2525

26+
/**
27+
* A single-quoted Ruby literal: unlike double quotes, Ruby doesn't interpolate
28+
* `#{}`, `#$` or `#@` in it, and only `\\` and `\'` are escapes.
29+
*/
30+
function toRubyString(value: string): string {
31+
return `'${value.replace(/[\\']/g, "\\$&")}'`;
32+
}
33+
2634
export class CocoaPodsService implements ICocoaPodsService {
2735
private static PODFILE_POST_INSTALL_SECTION_NAME = "post_install";
2836
private static INSTALLER_BLOCK_PARAMETER_NAME = "installer";
@@ -214,10 +222,24 @@ end`.trim();
214222
buildSettings: Record<string, string> | null,
215223
): Promise<void> {
216224
const { projectRoot } = platformData;
225+
const moduleName = "NativeScript-CLI-Build-Settings";
217226
const buildSettingsPodfile = path.join(
218227
projectRoot,
219228
"Podfile-build-settings",
220229
);
230+
231+
// Removed first so this section alone never keeps a Podfile alive: an app
232+
// without pods must not get one, or every prepare would run `pod install`.
233+
this.removePodfileFromProject(
234+
moduleName,
235+
buildSettingsPodfile,
236+
projectData,
237+
projectRoot,
238+
);
239+
if (!this.$fs.exists(this.getProjectPodfilePath(projectRoot))) {
240+
return;
241+
}
242+
221243
const deploymentTarget = this.$xcconfigService.readPropertyValue(
222244
path.join(
223245
projectData.appResourcesDirectoryPath,
@@ -230,41 +252,43 @@ end`.trim();
230252
const configLines = _.map(
231253
buildSettings,
232254
(value, key) =>
233-
` config.build_settings[${JSON.stringify(key)}] = ${JSON.stringify(value)}`,
255+
` config.build_settings[${toRubyString(key)}] = ${toRubyString(value)}`,
234256
);
235257
if (deploymentTarget) {
236258
configLines.push(
237259
` current = config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"]`,
238-
` if current.nil? || Gem::Version.new(current) < Gem::Version.new(${JSON.stringify(deploymentTarget)})`,
239-
` config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = ${JSON.stringify(deploymentTarget)}`,
260+
` if current.nil? || Gem::Version.new(current) < Gem::Version.new(${toRubyString(deploymentTarget)})`,
261+
` config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = ${toRubyString(deploymentTarget)}`,
240262
` end`,
241263
);
242264
}
243265

244-
if (configLines.length) {
245-
this.$fs.writeFile(
246-
buildSettingsPodfile,
247-
[
248-
"post_install do |installer|",
249-
" installer.pods_project.targets.each do |target|",
250-
" target.build_configurations.each do |config|",
251-
...configLines,
252-
" end",
253-
" end",
254-
"end",
255-
].join(EOL),
256-
);
266+
if (!configLines.length) {
267+
return;
257268
}
258269

259-
// Without a temporary Podfile this removes a previously applied section.
260-
await this.applyPodfileToProject(
261-
"NativeScript-CLI-Build-Settings",
270+
this.$fs.writeFile(
262271
buildSettingsPodfile,
263-
projectData,
264-
platformData,
272+
[
273+
"post_install do |installer|",
274+
" installer.pods_project.targets.each do |target|",
275+
" target.build_configurations.each do |config|",
276+
...configLines,
277+
" end",
278+
" end",
279+
"end",
280+
].join(EOL),
265281
);
266-
267-
this.$fs.deleteFile(buildSettingsPodfile);
282+
try {
283+
await this.applyPodfileToProject(
284+
moduleName,
285+
buildSettingsPodfile,
286+
projectData,
287+
platformData,
288+
);
289+
} finally {
290+
this.$fs.deleteFile(buildSettingsPodfile);
291+
}
268292
}
269293

270294
public async applyPodfileFromExtensions(
@@ -439,11 +463,16 @@ end`.trim();
439463
);
440464
const defaultContentWithPostInstallHook = `${defaultPodfileBeginning}${this.getPostInstallHookHeader()}end${EOL}end`;
441465
const defaultContentWithoutPostInstallHook = `${defaultPodfileBeginning}${EOL}end`;
442-
const trimmedProjectPodFileContent = projectPodFileContent.trim();
466+
// Removing sections leaves blank lines behind, so compare ignoring them.
467+
const normalize = (content: string) =>
468+
content.replace(/\s+/g, " ").trim();
469+
const normalizedProjectPodFileContent = normalize(projectPodFileContent);
443470
if (
444-
!trimmedProjectPodFileContent ||
445-
trimmedProjectPodFileContent === defaultContentWithPostInstallHook ||
446-
trimmedProjectPodFileContent === defaultContentWithoutPostInstallHook
471+
!normalizedProjectPodFileContent ||
472+
normalizedProjectPodFileContent ===
473+
normalize(defaultContentWithPostInstallHook) ||
474+
normalizedProjectPodFileContent ===
475+
normalize(defaultContentWithoutPostInstallHook)
447476
) {
448477
this.$fs.deleteFile(this.getProjectPodfilePath(projectRoot));
449478
} else {

‎test/cocoapods-service.ts‎

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,7 +1633,9 @@ end`,
16331633
describe("applyPodfileBuildSettings", () => {
16341634
const projectName = "projectName";
16351635

1636-
function setup(buildXcconfig: string) {
1636+
const pod = `pod 'Alamofire'`;
1637+
1638+
function setup(buildXcconfig: string, { withPodfile = true } = {}) {
16371639
const dir = mkdtempSync(path.join(tmpdir(), "podfile-deployment-target-"));
16381640
const testInjector = createTestInjector(FileSystem);
16391641
const fs: IFileSystem = testInjector.resolve("fs");
@@ -1655,10 +1657,12 @@ describe("applyPodfileBuildSettings", () => {
16551657
"iOS",
16561658
"build.xcconfig",
16571659
);
1658-
fs.writeFile(
1659-
podfilePath,
1660-
`use_frameworks!${EOL}${EOL}target "${projectName}" do${EOL}${EOL}end`,
1661-
);
1660+
if (withPodfile) {
1661+
fs.writeFile(
1662+
podfilePath,
1663+
`use_frameworks!${EOL}${EOL}target "${projectName}" do${EOL}${EOL}${pod}${EOL}${EOL}end`,
1664+
);
1665+
}
16621666
fs.writeFile(xcconfigPath, buildXcconfig);
16631667
const apply = (buildSettings: Record<string, string> = null) =>
16641668
cocoapodsService.applyPodfileBuildSettings(
@@ -1678,7 +1682,7 @@ describe("applyPodfileBuildSettings", () => {
16781682

16791683
const podfile = fs.readText(podfilePath);
16801684
assert.include(podfile, "# Begin Podfile - ");
1681-
assert.include(podfile, 'Gem::Version.new("18.0")');
1685+
assert.include(podfile, "Gem::Version.new('18.0')");
16821686
assert.include(podfile, "post_install do |installer|");
16831687
assert.isFalse(
16841688
fs.exists(path.join(platformData.projectRoot, "Podfile-build-settings")),
@@ -1693,7 +1697,7 @@ describe("applyPodfileBuildSettings", () => {
16931697
const podfile = fs.readText(podfilePath);
16941698
assert.include(
16951699
podfile,
1696-
'config.build_settings["COMPILATION_CACHE_ENABLE_CACHING"] = "YES"',
1700+
"config.build_settings['COMPILATION_CACHE_ENABLE_CACHING'] = 'YES'",
16971701
);
16981702
assert.notInclude(podfile, "IPHONEOS_DEPLOYMENT_TARGET");
16991703
});
@@ -1711,4 +1715,38 @@ describe("applyPodfileBuildSettings", () => {
17111715
assert.notInclude(podfile, "Podfile-build-settings");
17121716
assert.notInclude(podfile, "IPHONEOS_DEPLOYMENT_TARGET");
17131717
});
1718+
1719+
it("doesn't create a Podfile for an app without pods", async () => {
1720+
const { fs, podfilePath, apply } = setup(
1721+
"IPHONEOS_DEPLOYMENT_TARGET = 18.0;",
1722+
{ withPodfile: false },
1723+
);
1724+
1725+
await apply({ COMPILATION_CACHE_ENABLE_CACHING: "YES" });
1726+
1727+
assert.isFalse(fs.exists(podfilePath));
1728+
});
1729+
1730+
it("deletes the Podfile once these settings are all that's left in it", async () => {
1731+
const { fs, podfilePath, apply } = setup(
1732+
"IPHONEOS_DEPLOYMENT_TARGET = 18.0;",
1733+
);
1734+
await apply({ COMPILATION_CACHE_ENABLE_CACHING: "YES" });
1735+
fs.writeFile(podfilePath, fs.readText(podfilePath).replace(pod, ""));
1736+
1737+
await apply({ COMPILATION_CACHE_ENABLE_CACHING: "YES" });
1738+
1739+
assert.isFalse(fs.exists(podfilePath));
1740+
});
1741+
1742+
it("writes values as Ruby literals that aren't interpolated", async () => {
1743+
const { fs, podfilePath, apply } = setup("");
1744+
1745+
await apply({ COMPILATION_CACHE_CAS_PATH: `/a/#{x}/#$y/it's\\` });
1746+
1747+
assert.include(
1748+
fs.readText(podfilePath),
1749+
`config.build_settings['COMPILATION_CACHE_CAS_PATH'] = '/a/#{x}/#$y/it\\'s\\\\'`,
1750+
);
1751+
});
17141752
});

0 commit comments

Comments
 (0)