@invarn/cibuild 2.8.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,11 @@ afterEach(() => {
30
30
  function scheme(project, name, { runnable, testable, appExtension }) {
31
31
  const dir = join(root, `${project}.xcodeproj`, "xcshareddata", "xcschemes");
32
32
  mkdirSync(dir, { recursive: true });
33
+ // A `.xcodeproj` is its `project.pbxproj`; without one Xcode refuses to open
34
+ // it and the scanners now skip it, so a fixture that omits it is describing
35
+ // a directory that cannot exist outside a test. Content is irrelevant here —
36
+ // the scheme XML is what the ranking reads — but it has to be there.
37
+ writeFileSync(join(root, `${project}.xcodeproj`, "project.pbxproj"), "// !$*UTF8*$!\n");
33
38
  writeFileSync(join(dir, `${name}.xcscheme`), `<?xml version="1.0" encoding="UTF-8"?>
34
39
  <Scheme LastUpgradeVersion = "1600"${appExtension ? `\n wasCreatedForAppExtension = "YES"` : ""} version = "1.7">
35
40
  <TestAction buildConfiguration = "Debug">
@@ -1,5 +1,37 @@
1
1
  export type MobileProjectType = "android" | "ios" | "kmm";
2
2
  declare function isDirectory(path: string): boolean;
3
+ /**
4
+ * True when a path that is *named* like an Xcode container actually is one.
5
+ *
6
+ * **The** predicate — every question of the form "is this `.xcodeproj` a
7
+ * project" goes through here, and nothing else in cibuild joins a directory
8
+ * name to "there is a project there".
9
+ *
10
+ * A `.xcodeproj` is a directory whose whole content is `project.pbxproj`;
11
+ * without it `xcodebuild` refuses to open the project at all — "missing its
12
+ * project.pbxproj file". A `.xcworkspace` is `contents.xcworkspacedata` in
13
+ * the same way. Anything else ending in those suffixes is a directory with a
14
+ * suggestive name, and offering one is offering something Xcode would reject.
15
+ *
16
+ * `Uwi0/Oakane` is the row that found it. Its `iosApp/` ships two: a tracked
17
+ * `iosApp.xcodeproj` holding nothing but `project.xcworkspace/`, and the real
18
+ * `oakane.xcodeproj` beside it with a 20 217-byte `project.pbxproj`. The husk
19
+ * carries the name the KMM wizard gives, so the conventional name outranked
20
+ * the project that exists — and it supplied BOTH `IOS_PROJECT_PATH` and, via
21
+ * the scheme scan's project-name fallback, `IOS_SCHEME`. The build reached
22
+ * step 9 and died on "Unable to read project 'iosApp.xcodeproj'".
23
+ *
24
+ * This is the iOS twin of the Gradle module scan once treating "a directory
25
+ * holding a file called build.gradle" as a module: wrong in the same
26
+ * direction, and fixed the same way — one reader, used by everyone.
27
+ *
28
+ * The `.pbxproj` is deliberately NOT parsed. Its existence and non-emptiness
29
+ * is the whole question; a shared `.xcscheme` remains the oracle for what a
30
+ * project builds.
31
+ */
32
+ export declare function isXcodeContainer(path: string): boolean;
33
+ /** The entries of `dir` that are real Xcode containers, in `readdir` order. */
34
+ export declare function xcodeContainersIn(dir: string): string[];
3
35
  /**
4
36
  * Detects whether the given directory is the root of an Android, iOS, or
5
37
  * KMM project. Returns the detected project type, or null if none match.
@@ -1 +1 @@
1
- {"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../../src/shared/detect-project.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;AAoB1D,iBAAS,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM1C;AA+ED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAiC7E;AAED,oEAAoE;AACpE,OAAO,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../../src/shared/detect-project.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;AAoB1D,iBAAS,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM1C;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAYtD;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAYvD;AAuED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CA4B7E;AAED,oEAAoE;AACpE,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -33,6 +33,62 @@ function readOrNull(path) {
33
33
  return null;
34
34
  }
35
35
  }
36
+ /**
37
+ * True when a path that is *named* like an Xcode container actually is one.
38
+ *
39
+ * **The** predicate — every question of the form "is this `.xcodeproj` a
40
+ * project" goes through here, and nothing else in cibuild joins a directory
41
+ * name to "there is a project there".
42
+ *
43
+ * A `.xcodeproj` is a directory whose whole content is `project.pbxproj`;
44
+ * without it `xcodebuild` refuses to open the project at all — "missing its
45
+ * project.pbxproj file". A `.xcworkspace` is `contents.xcworkspacedata` in
46
+ * the same way. Anything else ending in those suffixes is a directory with a
47
+ * suggestive name, and offering one is offering something Xcode would reject.
48
+ *
49
+ * `Uwi0/Oakane` is the row that found it. Its `iosApp/` ships two: a tracked
50
+ * `iosApp.xcodeproj` holding nothing but `project.xcworkspace/`, and the real
51
+ * `oakane.xcodeproj` beside it with a 20 217-byte `project.pbxproj`. The husk
52
+ * carries the name the KMM wizard gives, so the conventional name outranked
53
+ * the project that exists — and it supplied BOTH `IOS_PROJECT_PATH` and, via
54
+ * the scheme scan's project-name fallback, `IOS_SCHEME`. The build reached
55
+ * step 9 and died on "Unable to read project 'iosApp.xcodeproj'".
56
+ *
57
+ * This is the iOS twin of the Gradle module scan once treating "a directory
58
+ * holding a file called build.gradle" as a module: wrong in the same
59
+ * direction, and fixed the same way — one reader, used by everyone.
60
+ *
61
+ * The `.pbxproj` is deliberately NOT parsed. Its existence and non-emptiness
62
+ * is the whole question; a shared `.xcscheme` remains the oracle for what a
63
+ * project builds.
64
+ */
65
+ export function isXcodeContainer(path) {
66
+ const manifest = path.endsWith(".xcworkspace")
67
+ ? "contents.xcworkspacedata"
68
+ : path.endsWith(".xcodeproj")
69
+ ? "project.pbxproj"
70
+ : null;
71
+ if (manifest === null)
72
+ return false;
73
+ try {
74
+ return statSync(resolve(path, manifest)).size > 0;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /** The entries of `dir` that are real Xcode containers, in `readdir` order. */
81
+ export function xcodeContainersIn(dir) {
82
+ let entries;
83
+ try {
84
+ entries = readdirSync(dir);
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ return entries.filter((e) => (e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace")) &&
90
+ isXcodeContainer(resolve(dir, e)));
91
+ }
36
92
  /** True when this directory holds something an Apple build reads. */
37
93
  function holdsAppleConsumer(dir) {
38
94
  let entries;
@@ -42,7 +98,7 @@ function holdsAppleConsumer(dir) {
42
98
  catch {
43
99
  return false;
44
100
  }
45
- if (entries.some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"))) {
101
+ if (xcodeContainersIn(dir).length > 0) {
46
102
  return true;
47
103
  }
48
104
  if (entries.includes("Podfile"))
@@ -144,15 +200,11 @@ export function detectMobileProjectRoot(dir) {
144
200
  const iosFileIndicators = ["Podfile"];
145
201
  const hasIosFile = iosFileIndicators.some((f) => existsSync(resolve(dir, f)));
146
202
  if (!hasIosFile) {
147
- try {
148
- const entries = readdirSync(dir);
149
- const hasXcodeDir = entries.some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
150
- if (hasXcodeDir)
151
- return "ios";
152
- }
153
- catch {
154
- // Unreadable directory — fall through.
155
- }
203
+ // A husk — a directory named `*.xcodeproj` with no `project.pbxproj` —
204
+ // does not make this an iOS project root. Saying `ios` on the strength of
205
+ // one writes a pipeline aimed at something xcodebuild cannot open.
206
+ if (xcodeContainersIn(dir).length > 0)
207
+ return "ios";
156
208
  }
157
209
  else {
158
210
  return "ios";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=xcode-container.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xcode-container.test.d.ts","sourceRoot":"","sources":["../../../src/shared/xcode-container.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * A `.xcodeproj` with no `project.pbxproj` is not a project.
3
+ *
4
+ * `Uwi0/Oakane` is the row. Its `iosApp/` ships two Xcode projects: a tracked
5
+ * `iosApp.xcodeproj` holding nothing but `project.xcworkspace/`, and the real
6
+ * `oakane.xcodeproj` beside it. The scan chose the husk — `iosApp` is the name
7
+ * the KMM wizard gives, so the conventional name outranked the project that
8
+ * exists — and the build died at step 9 on
9
+ *
10
+ * xcodebuild: error: Unable to read project 'iosApp.xcodeproj' …
11
+ * Reason: … missing its project.pbxproj file.
12
+ *
13
+ * The husk supplied `IOS_SCHEME` too: with no shared schemes anywhere in that
14
+ * repository, `detectSchemes` falls back to each project's own directory name,
15
+ * so the husk contributed the scheme `iosApp` as well as the project path.
16
+ *
17
+ * This is the iOS twin of the Gradle module scan once treating "a directory
18
+ * holding a file called build.gradle" as a module, and it is fixed the same
19
+ * way: one reader, used by everyone.
20
+ */
21
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { tmpdir } from "node:os";
24
+ import { isXcodeContainer, xcodeContainersIn, detectMobileProjectRoot } from "./detect-project.js";
25
+ import { scanIosProject } from "../commands/ios-scanner.js";
26
+ let root;
27
+ beforeEach(() => {
28
+ root = mkdtempSync(join(tmpdir(), "cibuild-husk-"));
29
+ });
30
+ afterEach(() => {
31
+ rmSync(root, { recursive: true, force: true });
32
+ });
33
+ /** A project Xcode could open. */
34
+ function project(name) {
35
+ mkdirSync(join(root, `${name}.xcodeproj`), { recursive: true });
36
+ writeFileSync(join(root, `${name}.xcodeproj`, "project.pbxproj"), "// !$*UTF8*$!\n");
37
+ }
38
+ /** Oakane's shape: the name, the inner workspace, and no project at all. */
39
+ function husk(name) {
40
+ mkdirSync(join(root, `${name}.xcodeproj`, "project.xcworkspace"), { recursive: true });
41
+ writeFileSync(join(root, `${name}.xcodeproj`, "project.xcworkspace", "contents.xcworkspacedata"), '<?xml version="1.0" encoding="UTF-8"?>\n<Workspace version = "1.0"></Workspace>\n');
42
+ }
43
+ function workspace(name) {
44
+ mkdirSync(join(root, `${name}.xcworkspace`), { recursive: true });
45
+ writeFileSync(join(root, `${name}.xcworkspace`, "contents.xcworkspacedata"), '<?xml version="1.0" encoding="UTF-8"?>\n<Workspace version = "1.0"></Workspace>\n');
46
+ }
47
+ describe("isXcodeContainer", () => {
48
+ test("a project with a project.pbxproj is one", () => {
49
+ project("Real");
50
+ expect(isXcodeContainer(join(root, "Real.xcodeproj"))).toBe(true);
51
+ });
52
+ test("a project with no project.pbxproj is not", () => {
53
+ husk("iosApp");
54
+ expect(isXcodeContainer(join(root, "iosApp.xcodeproj"))).toBe(false);
55
+ });
56
+ test("an empty project.pbxproj is not a project either", () => {
57
+ mkdirSync(join(root, "Empty.xcodeproj"), { recursive: true });
58
+ writeFileSync(join(root, "Empty.xcodeproj", "project.pbxproj"), "");
59
+ expect(isXcodeContainer(join(root, "Empty.xcodeproj"))).toBe(false);
60
+ });
61
+ test("a workspace needs its contents.xcworkspacedata", () => {
62
+ workspace("Real");
63
+ mkdirSync(join(root, "Husk.xcworkspace"), { recursive: true });
64
+ expect(isXcodeContainer(join(root, "Real.xcworkspace"))).toBe(true);
65
+ expect(isXcodeContainer(join(root, "Husk.xcworkspace"))).toBe(false);
66
+ });
67
+ test("a path that is not named like a container is never one", () => {
68
+ mkdirSync(join(root, "Sources"), { recursive: true });
69
+ expect(isXcodeContainer(join(root, "Sources"))).toBe(false);
70
+ expect(isXcodeContainer(join(root, "does-not-exist"))).toBe(false);
71
+ });
72
+ });
73
+ describe("xcodeContainersIn", () => {
74
+ test("keeps the real projects and drops the husks", () => {
75
+ husk("iosApp");
76
+ project("oakane");
77
+ expect(xcodeContainersIn(root)).toEqual(["oakane.xcodeproj"]);
78
+ });
79
+ test("an unreadable directory is empty, not a throw", () => {
80
+ expect(xcodeContainersIn(join(root, "nowhere"))).toEqual([]);
81
+ });
82
+ });
83
+ describe("detectMobileProjectRoot", () => {
84
+ test("a husk alone is not an iOS project root", () => {
85
+ husk("iosApp");
86
+ expect(detectMobileProjectRoot(root)).toBeNull();
87
+ });
88
+ test("a real project beside a husk still is", () => {
89
+ husk("iosApp");
90
+ project("oakane");
91
+ expect(detectMobileProjectRoot(root)).toBe("ios");
92
+ });
93
+ });
94
+ describe("scanIosProject", () => {
95
+ test("Oakane's shape resolves the project that exists, and its scheme", async () => {
96
+ husk("iosApp");
97
+ project("oakane");
98
+ const scan = await scanIosProject(root);
99
+ expect(scan.projectPath).toBe("oakane.xcodeproj");
100
+ // No shared schemes anywhere, so each project falls back to its own name —
101
+ // and only one of them is a project.
102
+ expect(scan.detectedSchemes).toEqual(["oakane"]);
103
+ });
104
+ test("a repository whose only project is a husk resolves nothing", async () => {
105
+ husk("iosApp");
106
+ const scan = await scanIosProject(root);
107
+ expect(scan.projectPath).toBe("");
108
+ expect(scan.detectedSchemes).toEqual([]);
109
+ });
110
+ test("a single valid project is unchanged", async () => {
111
+ project("Contributions");
112
+ const scan = await scanIosProject(root);
113
+ expect(scan.projectPath).toBe("Contributions.xcodeproj");
114
+ expect(scan.detectedSchemes).toEqual(["Contributions"]);
115
+ });
116
+ test("a workspace still outranks a project beside it", async () => {
117
+ project("QiniuSDK");
118
+ workspace("QiniuSDK");
119
+ const scan = await scanIosProject(root);
120
+ expect(scan.projectPath).toBe("QiniuSDK.xcworkspace");
121
+ });
122
+ });
123
+ //# sourceMappingURL=xcode-container.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",