@better-update/cli 0.25.0 → 0.26.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.
- package/dist/index.mjs +805 -208
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import AppleUtils from "@expo/apple-utils";
|
|
|
11
11
|
import { autocomplete, cancel, confirm, isCancel, multiselect, password, select, text } from "@clack/prompts";
|
|
12
12
|
import { open, readFile, writeFile } from "node:fs/promises";
|
|
13
13
|
import { X509Certificate, createHash, createSign, createVerify, randomBytes, randomUUID } from "node:crypto";
|
|
14
|
-
import { accessSync, chmodSync, constants, createReadStream,
|
|
14
|
+
import { accessSync, chmodSync, constants, createReadStream, promises } from "node:fs";
|
|
15
15
|
import { Entry } from "@napi-rs/keyring";
|
|
16
16
|
import { once } from "node:events";
|
|
17
17
|
import { createServer } from "node:http";
|
|
@@ -34,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region package.json
|
|
37
|
-
var version = "0.
|
|
37
|
+
var version = "0.26.1";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/lib/interactive-mode.ts
|
|
@@ -256,7 +256,7 @@ var AnalyticsGroup = class extends HttpApiGroup.make("analytics").add(HttpApiEnd
|
|
|
256
256
|
|
|
257
257
|
//#endregion
|
|
258
258
|
//#region ../../packages/api/src/domain/android-application-identifier.ts
|
|
259
|
-
const AndroidPackageName = Schema.String.pipe(Schema.pattern(/^[A-Za-z][A-Za-z0-9_]*(
|
|
259
|
+
const AndroidPackageName = Schema.String.pipe(Schema.pattern(/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u, { message: () => "Package name must be reverse-domain style (e.g., com.acme.app)" }));
|
|
260
260
|
var AndroidApplicationIdentifier = class extends Schema.Class("AndroidApplicationIdentifier")({
|
|
261
261
|
id: Id,
|
|
262
262
|
organizationId: Id,
|
|
@@ -4751,8 +4751,31 @@ const findIosArtifact = ({ exportPath }) => Effect.gen(function* () {
|
|
|
4751
4751
|
if (!picked) return yield* new ArtifactNotFoundError({ message: `No .ipa file found under "${exportPath}".` });
|
|
4752
4752
|
return picked.path;
|
|
4753
4753
|
});
|
|
4754
|
-
|
|
4755
|
-
|
|
4754
|
+
/**
|
|
4755
|
+
* Resolve a custom-command build artifact from a user-supplied path. A pattern
|
|
4756
|
+
* without wildcards is treated as a literal path (relative to `baseDir`);
|
|
4757
|
+
* otherwise the fixed leading directory + file extension are extracted and the
|
|
4758
|
+
* newest matching file under that directory is returned.
|
|
4759
|
+
*/
|
|
4760
|
+
const findArtifactByGlob = ({ baseDir, pattern, minMtimeMs }) => Effect.gen(function* () {
|
|
4761
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4762
|
+
if (!/[*?[]/u.test(pattern)) {
|
|
4763
|
+
const full = path.isAbsolute(pattern) ? pattern : path.join(baseDir, pattern);
|
|
4764
|
+
if (yield* fs.exists(full).pipe(Effect.orElseSucceed(() => false))) return full;
|
|
4765
|
+
return yield* new ArtifactNotFoundError({ message: `No artifact found at "${full}".` });
|
|
4766
|
+
}
|
|
4767
|
+
const extension = path.extname(pattern).toLowerCase();
|
|
4768
|
+
if (extension === "") return yield* new ArtifactNotFoundError({ message: `artifactPath "${pattern}" must end in a file extension (e.g. **/*.aab).` });
|
|
4769
|
+
const wildcardIndex = pattern.search(/[*?[]/u);
|
|
4770
|
+
const fixedPrefix = pattern.slice(0, wildcardIndex);
|
|
4771
|
+
const prefixDir = fixedPrefix.includes("/") ? fixedPrefix.slice(0, fixedPrefix.lastIndexOf("/")) : "";
|
|
4772
|
+
const searchRoot = prefixDir === "" ? baseDir : path.join(baseDir, prefixDir);
|
|
4773
|
+
const picked = newest(yield* walkAndFind(searchRoot, extension), minMtimeMs);
|
|
4774
|
+
if (!picked) return yield* new ArtifactNotFoundError({ message: `No file matching "${pattern}" found under "${searchRoot}".` });
|
|
4775
|
+
return picked.path;
|
|
4776
|
+
});
|
|
4777
|
+
const findAndroidArtifact = ({ projectRoot, format, flavor, buildType, minMtimeMs, module: gradleModule = "app" }) => Effect.gen(function* () {
|
|
4778
|
+
const outputsRoot = path.join(projectRoot, "android", gradleModule, "build", "outputs");
|
|
4756
4779
|
const subdir = format === "aab" ? "bundle" : "apk";
|
|
4757
4780
|
const variantDir = flavor ? `${flavor}${capitalize(buildType)}` : buildType;
|
|
4758
4781
|
const pickedDirect = newest(yield* walkAndFind(path.join(outputsRoot, subdir, variantDir), `.${format}`), minMtimeMs);
|
|
@@ -18940,7 +18963,7 @@ const stringField = (cert, name) => {
|
|
|
18940
18963
|
return typeof value === "string" ? value : null;
|
|
18941
18964
|
};
|
|
18942
18965
|
const matchTeamFromCommonName = (cn) => {
|
|
18943
|
-
const match = /\(([A-Z0-9]{10})\)/u.exec(cn);
|
|
18966
|
+
const match = /\((?<team>[A-Z0-9]{10})\)/u.exec(cn);
|
|
18944
18967
|
if (match === null) return null;
|
|
18945
18968
|
const [, captured] = match;
|
|
18946
18969
|
return captured === void 0 ? null : captured;
|
|
@@ -20025,56 +20048,44 @@ const gradleTaskName = (format, flavor, buildType) => {
|
|
|
20025
20048
|
const verb = format === "aab" ? "bundle" : "assemble";
|
|
20026
20049
|
return flavor ? `${verb}${capitalize(flavor)}${capitalize(buildType)}` : `${verb}${capitalize(buildType)}`;
|
|
20027
20050
|
};
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20051
|
+
/** Resolve the signing keystore (remote or local), or `undefined` when skipped. */
|
|
20052
|
+
const resolveAndroidCredentials = (input) => {
|
|
20053
|
+
if (input.skipCredentials) return Effect.succeed(void 0);
|
|
20054
|
+
return input.credentialsSource === "local" ? loadLocalAndroidCredentials({ projectRoot: input.projectRoot }) : downloadAndroidCredentials(input.api, {
|
|
20055
|
+
projectId: input.projectId,
|
|
20056
|
+
applicationIdentifier: input.applicationIdentifier,
|
|
20057
|
+
tempDir: input.tempDir,
|
|
20058
|
+
buildProfile: input.profileName
|
|
20059
|
+
});
|
|
20060
|
+
};
|
|
20061
|
+
/** Gradle build against the (already-prepared) `android/` dir. */
|
|
20062
|
+
const runGradleBuild = (input, commandEnv) => Effect.gen(function* () {
|
|
20031
20063
|
const buildStartMs = Date.now();
|
|
20032
|
-
const { format } = androidProfile;
|
|
20033
|
-
const
|
|
20034
|
-
const
|
|
20035
|
-
const androidDir = path.join(projectRoot, "android");
|
|
20036
|
-
const
|
|
20037
|
-
yield*
|
|
20038
|
-
command: "bunx",
|
|
20039
|
-
args: [
|
|
20040
|
-
"expo",
|
|
20041
|
-
"prebuild",
|
|
20042
|
-
"--platform",
|
|
20043
|
-
"android",
|
|
20044
|
-
"--clean"
|
|
20045
|
-
],
|
|
20046
|
-
cwd: projectRoot,
|
|
20047
|
-
env: commandEnv
|
|
20048
|
-
}, "expo prebuild android");
|
|
20049
|
-
const gradleArgs = yield* input.skipCredentials ? Effect.succeed([]) : Effect.gen(function* () {
|
|
20050
|
-
const credentials = input.credentialsSource === "local" ? yield* loadLocalAndroidCredentials({ projectRoot }) : yield* downloadAndroidCredentials(api, {
|
|
20051
|
-
projectId,
|
|
20052
|
-
applicationIdentifier,
|
|
20053
|
-
tempDir,
|
|
20054
|
-
buildProfile: input.profileName
|
|
20055
|
-
});
|
|
20064
|
+
const { format, flavor } = input.androidProfile;
|
|
20065
|
+
const buildType = input.androidProfile.buildType ?? "release";
|
|
20066
|
+
const moduleName = input.androidProfile.module ?? "app";
|
|
20067
|
+
const androidDir = path.join(input.projectRoot, "android");
|
|
20068
|
+
const credentials = yield* resolveAndroidCredentials(input);
|
|
20069
|
+
const gradleArgs = credentials === void 0 ? [] : yield* Effect.gen(function* () {
|
|
20056
20070
|
const fs = yield* FileSystem.FileSystem;
|
|
20057
|
-
const signingGradlePath = path.join(tempDir, "signing.gradle");
|
|
20058
|
-
yield* fs.writeFileString(signingGradlePath, renderSigningGradle(
|
|
20059
|
-
keystorePath: credentials.keystorePath,
|
|
20060
|
-
storePassword: credentials.storePassword,
|
|
20061
|
-
keyAlias: credentials.keyAlias,
|
|
20062
|
-
keyPassword: credentials.keyPassword
|
|
20063
|
-
}));
|
|
20071
|
+
const signingGradlePath = path.join(input.tempDir, "signing.gradle");
|
|
20072
|
+
yield* fs.writeFileString(signingGradlePath, renderSigningGradle(credentials));
|
|
20064
20073
|
return ["--init-script", signingGradlePath];
|
|
20065
20074
|
});
|
|
20066
|
-
const taskName = gradleTaskName(format, flavor, buildType);
|
|
20075
|
+
const taskName = input.androidProfile.gradleTask ?? gradleTaskName(format, flavor, buildType);
|
|
20076
|
+
const taskArg = taskName.startsWith(":") ? taskName : `:${moduleName}:${taskName}`;
|
|
20067
20077
|
yield* runStep({
|
|
20068
20078
|
command: "./gradlew",
|
|
20069
|
-
args: [...gradleArgs,
|
|
20079
|
+
args: [...gradleArgs, taskArg],
|
|
20070
20080
|
cwd: androidDir,
|
|
20071
20081
|
env: commandEnv
|
|
20072
20082
|
}, "gradlew");
|
|
20073
20083
|
const artifactPath = yield* findAndroidArtifact({
|
|
20074
|
-
projectRoot,
|
|
20084
|
+
projectRoot: input.projectRoot,
|
|
20075
20085
|
format,
|
|
20076
20086
|
buildType,
|
|
20077
20087
|
minMtimeMs: buildStartMs,
|
|
20088
|
+
module: moduleName,
|
|
20078
20089
|
...compact({ flavor })
|
|
20079
20090
|
});
|
|
20080
20091
|
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
@@ -20084,6 +20095,71 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
|
20084
20095
|
sha256
|
|
20085
20096
|
};
|
|
20086
20097
|
});
|
|
20098
|
+
/**
|
|
20099
|
+
* Custom-command build. We can't inject signing into an arbitrary build, so the
|
|
20100
|
+
* resolved keystore + passwords are exposed to the command as `BETTER_UPDATE_*`
|
|
20101
|
+
* env vars; the user's script consumes them. The artifact is located via the
|
|
20102
|
+
* profile's `artifactPath` glob.
|
|
20103
|
+
*/
|
|
20104
|
+
const runAndroidCustom = (input, commandEnv) => Effect.gen(function* () {
|
|
20105
|
+
const buildStartMs = Date.now();
|
|
20106
|
+
const custom = input.customCommand;
|
|
20107
|
+
if (custom === void 0) return yield* new BuildFailedError({
|
|
20108
|
+
step: "custom android build",
|
|
20109
|
+
exitCode: 1,
|
|
20110
|
+
message: "Internal: custom Android strategy selected without a custom command."
|
|
20111
|
+
});
|
|
20112
|
+
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
20113
|
+
step: "custom android build",
|
|
20114
|
+
exitCode: 1,
|
|
20115
|
+
message: "Custom Android build requires \"artifactPath\" (e.g. \"**/*.aab\") in better-update.json."
|
|
20116
|
+
});
|
|
20117
|
+
const credentials = yield* resolveAndroidCredentials(input);
|
|
20118
|
+
const credEnv = credentials === void 0 ? {} : {
|
|
20119
|
+
BETTER_UPDATE_ANDROID_KEYSTORE_PATH: credentials.keystorePath,
|
|
20120
|
+
BETTER_UPDATE_ANDROID_KEYSTORE_PASSWORD: credentials.storePassword,
|
|
20121
|
+
BETTER_UPDATE_ANDROID_KEY_ALIAS: credentials.keyAlias,
|
|
20122
|
+
BETTER_UPDATE_ANDROID_KEY_PASSWORD: credentials.keyPassword
|
|
20123
|
+
};
|
|
20124
|
+
const cwd = custom.cwd === void 0 ? input.projectRoot : path.join(input.projectRoot, custom.cwd);
|
|
20125
|
+
yield* runStep({
|
|
20126
|
+
command: "sh",
|
|
20127
|
+
args: ["-c", custom.command],
|
|
20128
|
+
cwd,
|
|
20129
|
+
env: {
|
|
20130
|
+
...commandEnv,
|
|
20131
|
+
...credEnv,
|
|
20132
|
+
...custom.env
|
|
20133
|
+
}
|
|
20134
|
+
}, "custom android build");
|
|
20135
|
+
const artifactPath = yield* findArtifactByGlob({
|
|
20136
|
+
baseDir: cwd,
|
|
20137
|
+
pattern: custom.artifactPath,
|
|
20138
|
+
minMtimeMs: buildStartMs
|
|
20139
|
+
});
|
|
20140
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
20141
|
+
return {
|
|
20142
|
+
artifactPath,
|
|
20143
|
+
byteSize,
|
|
20144
|
+
sha256
|
|
20145
|
+
};
|
|
20146
|
+
});
|
|
20147
|
+
const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
20148
|
+
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
20149
|
+
if (input.strategy === "expo") yield* runStep({
|
|
20150
|
+
command: "bunx",
|
|
20151
|
+
args: [
|
|
20152
|
+
"expo",
|
|
20153
|
+
"prebuild",
|
|
20154
|
+
"--platform",
|
|
20155
|
+
"android",
|
|
20156
|
+
"--clean"
|
|
20157
|
+
],
|
|
20158
|
+
cwd: input.projectRoot,
|
|
20159
|
+
env: commandEnv
|
|
20160
|
+
}, "expo prebuild android");
|
|
20161
|
+
return input.strategy === "custom" ? yield* runAndroidCustom(input, commandEnv) : yield* runGradleBuild(input, commandEnv);
|
|
20162
|
+
});
|
|
20087
20163
|
|
|
20088
20164
|
//#endregion
|
|
20089
20165
|
//#region src/lib/credentials-generator-apple-id.ts
|
|
@@ -20798,7 +20874,7 @@ const listCurrentKeychains = Effect.gen(function* () {
|
|
|
20798
20874
|
const parseSigningIdentity = (output) => {
|
|
20799
20875
|
const lines = output.split("\n");
|
|
20800
20876
|
for (const line of lines) {
|
|
20801
|
-
const match = /"([^"]+)"/u.exec(line);
|
|
20877
|
+
const match = /"(?<identity>[^"]+)"/u.exec(line);
|
|
20802
20878
|
if (match?.[1]) return match[1];
|
|
20803
20879
|
}
|
|
20804
20880
|
};
|
|
@@ -21101,36 +21177,85 @@ const createXcodebuildFormatter = (projectRoot) => {
|
|
|
21101
21177
|
};
|
|
21102
21178
|
|
|
21103
21179
|
//#endregion
|
|
21104
|
-
//#region src/commands/build/ios.ts
|
|
21105
|
-
const
|
|
21106
|
-
|
|
21107
|
-
|
|
21108
|
-
|
|
21180
|
+
//#region src/commands/build/ios-prepare.ts
|
|
21181
|
+
const baseName = (entry) => entry.replace(/\.(?<ext>xcworkspace|xcodeproj)$/u, "");
|
|
21182
|
+
/**
|
|
21183
|
+
* Resolve the Xcode container to build: an explicit `workspace`/`project` from
|
|
21184
|
+
* the profile, else an auto-discovered `.xcworkspace` (CocoaPods), else the
|
|
21185
|
+
* `.xcodeproj` (pure-native apps without Pods).
|
|
21186
|
+
*/
|
|
21187
|
+
const resolveXcodeContainer = (projectRoot, iosDir, iosProfile) => Effect.gen(function* () {
|
|
21188
|
+
if (iosProfile.workspace !== void 0) {
|
|
21189
|
+
const containerPath = path.resolve(projectRoot, iosProfile.workspace);
|
|
21190
|
+
return {
|
|
21191
|
+
flag: "-workspace",
|
|
21192
|
+
containerPath,
|
|
21193
|
+
schemeBase: baseName(path.basename(containerPath))
|
|
21194
|
+
};
|
|
21195
|
+
}
|
|
21196
|
+
if (iosProfile.project !== void 0) {
|
|
21197
|
+
const containerPath = path.resolve(projectRoot, iosProfile.project);
|
|
21198
|
+
return {
|
|
21199
|
+
flag: "-project",
|
|
21200
|
+
containerPath,
|
|
21201
|
+
schemeBase: baseName(path.basename(containerPath))
|
|
21202
|
+
};
|
|
21203
|
+
}
|
|
21204
|
+
const entries = yield* (yield* FileSystem.FileSystem).readDirectory(iosDir).pipe(Effect.orElseSucceed(() => []));
|
|
21205
|
+
const workspace = entries.find((entry) => entry.endsWith(".xcworkspace"));
|
|
21206
|
+
if (workspace !== void 0) return {
|
|
21207
|
+
flag: "-workspace",
|
|
21208
|
+
containerPath: path.join(iosDir, workspace),
|
|
21209
|
+
schemeBase: baseName(workspace)
|
|
21210
|
+
};
|
|
21211
|
+
const project = entries.find((entry) => entry.endsWith(".xcodeproj"));
|
|
21212
|
+
if (project !== void 0) return {
|
|
21213
|
+
flag: "-project",
|
|
21214
|
+
containerPath: path.join(iosDir, project),
|
|
21215
|
+
schemeBase: baseName(project)
|
|
21216
|
+
};
|
|
21217
|
+
return yield* new BuildFailedError({
|
|
21218
|
+
step: "resolve Xcode container",
|
|
21109
21219
|
exitCode: 1,
|
|
21110
|
-
message: `No .xcworkspace found under ${iosDir}.
|
|
21220
|
+
message: `No .xcworkspace or .xcodeproj found under ${iosDir}. Set ios.workspace / ios.project in better-update.json.`
|
|
21111
21221
|
});
|
|
21112
|
-
return workspace;
|
|
21113
21222
|
});
|
|
21114
|
-
|
|
21115
|
-
|
|
21116
|
-
|
|
21117
|
-
|
|
21118
|
-
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
"
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
|
|
21126
|
-
|
|
21127
|
-
|
|
21223
|
+
/**
|
|
21224
|
+
* Prepare the `ios/` dir for an xcodebuild. Expo regenerates it from app.json
|
|
21225
|
+
* via prebuild then runs `pod install`; bare/KMP/native build the committed dir
|
|
21226
|
+
* and only run `pod install` when a Podfile is present (unless disabled).
|
|
21227
|
+
*/
|
|
21228
|
+
const prepareIosNative = (params) => Effect.gen(function* () {
|
|
21229
|
+
if (params.strategy === "expo") {
|
|
21230
|
+
yield* runStep({
|
|
21231
|
+
command: "bunx",
|
|
21232
|
+
args: [
|
|
21233
|
+
"expo",
|
|
21234
|
+
"prebuild",
|
|
21235
|
+
"--platform",
|
|
21236
|
+
"ios",
|
|
21237
|
+
"--clean"
|
|
21238
|
+
],
|
|
21239
|
+
cwd: params.projectRoot,
|
|
21240
|
+
env: params.commandEnv
|
|
21241
|
+
}, "expo prebuild ios");
|
|
21242
|
+
yield* runStep({
|
|
21243
|
+
command: "pod",
|
|
21244
|
+
args: ["install"],
|
|
21245
|
+
cwd: params.iosDir,
|
|
21246
|
+
env: params.commandEnv
|
|
21247
|
+
}, "pod install");
|
|
21248
|
+
return;
|
|
21249
|
+
}
|
|
21250
|
+
if (params.iosProfile.podInstall === false) return;
|
|
21251
|
+
if (yield* (yield* FileSystem.FileSystem).exists(path.join(params.iosDir, "Podfile")).pipe(Effect.orElseSucceed(() => false))) yield* runStep({
|
|
21128
21252
|
command: "pod",
|
|
21129
21253
|
args: ["install"],
|
|
21130
21254
|
cwd: params.iosDir,
|
|
21131
21255
|
env: params.commandEnv
|
|
21132
21256
|
}, "pod install");
|
|
21133
21257
|
});
|
|
21258
|
+
/** Recursively locate the first `.app` bundle under `root` (simulator output). */
|
|
21134
21259
|
const findAppDirectory = (root) => Effect.gen(function* () {
|
|
21135
21260
|
const fs = yield* FileSystem.FileSystem;
|
|
21136
21261
|
const stack = [root];
|
|
@@ -21150,25 +21275,30 @@ const findAppDirectory = (root) => Effect.gen(function* () {
|
|
|
21150
21275
|
}
|
|
21151
21276
|
return yield* new ArtifactNotFoundError({ message: `No .app bundle found under "${root}".` });
|
|
21152
21277
|
});
|
|
21278
|
+
|
|
21279
|
+
//#endregion
|
|
21280
|
+
//#region src/commands/build/ios.ts
|
|
21153
21281
|
const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
21154
21282
|
const { projectRoot, iosProfile, envVars, tempDir } = input;
|
|
21155
21283
|
const runtime = yield* CliRuntime;
|
|
21156
21284
|
const iosDir = path.join(projectRoot, "ios");
|
|
21157
21285
|
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
21158
|
-
yield*
|
|
21286
|
+
yield* prepareIosNative({
|
|
21287
|
+
strategy: input.strategy,
|
|
21159
21288
|
projectRoot,
|
|
21160
21289
|
iosDir,
|
|
21290
|
+
iosProfile,
|
|
21161
21291
|
commandEnv
|
|
21162
21292
|
});
|
|
21163
|
-
const
|
|
21164
|
-
const scheme = iosProfile.scheme ??
|
|
21293
|
+
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21294
|
+
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
21165
21295
|
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
21166
21296
|
const derivedDataPath = path.join(tempDir, "derived-data");
|
|
21167
21297
|
const buildCmd = {
|
|
21168
21298
|
command: "xcodebuild",
|
|
21169
21299
|
args: [
|
|
21170
|
-
|
|
21171
|
-
|
|
21300
|
+
container.flag,
|
|
21301
|
+
container.containerPath,
|
|
21172
21302
|
"-scheme",
|
|
21173
21303
|
scheme,
|
|
21174
21304
|
"-configuration",
|
|
@@ -21256,13 +21386,15 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21256
21386
|
const iosDir = path.join(projectRoot, "ios");
|
|
21257
21387
|
const { distribution } = iosProfile;
|
|
21258
21388
|
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
21259
|
-
yield*
|
|
21389
|
+
yield* prepareIosNative({
|
|
21390
|
+
strategy: input.strategy,
|
|
21260
21391
|
projectRoot,
|
|
21261
21392
|
iosDir,
|
|
21393
|
+
iosProfile,
|
|
21262
21394
|
commandEnv
|
|
21263
21395
|
});
|
|
21264
|
-
const
|
|
21265
|
-
const scheme = iosProfile.scheme ??
|
|
21396
|
+
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21397
|
+
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
21266
21398
|
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
21267
21399
|
const signedTargets = yield* discoverSignedTargets({
|
|
21268
21400
|
iosDir,
|
|
@@ -21309,8 +21441,8 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21309
21441
|
const archiveCmd = {
|
|
21310
21442
|
command: "xcodebuild",
|
|
21311
21443
|
args: [
|
|
21312
|
-
|
|
21313
|
-
|
|
21444
|
+
container.flag,
|
|
21445
|
+
container.containerPath,
|
|
21314
21446
|
"-scheme",
|
|
21315
21447
|
scheme,
|
|
21316
21448
|
"-configuration",
|
|
@@ -21374,19 +21506,76 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21374
21506
|
sha256
|
|
21375
21507
|
};
|
|
21376
21508
|
});
|
|
21377
|
-
|
|
21509
|
+
/**
|
|
21510
|
+
* Custom-command iOS build. The resolved p12 + provisioning profiles are written
|
|
21511
|
+
* to `tempDir` and their paths exposed via `BETTER_UPDATE_IOS_*` env vars; the
|
|
21512
|
+
* user's command performs the actual signing/archive. The artifact is located
|
|
21513
|
+
* via the profile's `artifactPath` glob.
|
|
21514
|
+
*/
|
|
21515
|
+
const runIosCustom = (input) => Effect.gen(function* () {
|
|
21516
|
+
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
21517
|
+
const custom = input.customCommand;
|
|
21518
|
+
if (custom === void 0) return yield* new BuildFailedError({
|
|
21519
|
+
step: "custom ios build",
|
|
21520
|
+
exitCode: 1,
|
|
21521
|
+
message: "Internal: custom iOS strategy selected without a custom command."
|
|
21522
|
+
});
|
|
21523
|
+
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
21524
|
+
step: "custom ios build",
|
|
21525
|
+
exitCode: 1,
|
|
21526
|
+
message: "Custom iOS build requires \"artifactPath\" (e.g. \"build/*.ipa\") in better-update.json."
|
|
21527
|
+
});
|
|
21528
|
+
const credentials = yield* fetchAllCredentials({
|
|
21529
|
+
api: input.api,
|
|
21530
|
+
input,
|
|
21531
|
+
mainBundleIdentifier: input.bundleId,
|
|
21532
|
+
allBundleIdentifiers: [input.bundleId]
|
|
21533
|
+
});
|
|
21534
|
+
const credEnv = {
|
|
21535
|
+
BETTER_UPDATE_IOS_P12_PATH: credentials.p12Path,
|
|
21536
|
+
BETTER_UPDATE_IOS_P12_PASSWORD: credentials.p12Password,
|
|
21537
|
+
BETTER_UPDATE_IOS_PROVISIONING_PROFILES: credentials.profiles.map((profile) => profile.profilePath).join(":")
|
|
21538
|
+
};
|
|
21539
|
+
const cwd = custom.cwd === void 0 ? input.projectRoot : path.join(input.projectRoot, custom.cwd);
|
|
21540
|
+
const buildStartMs = Date.now();
|
|
21541
|
+
yield* runStep({
|
|
21542
|
+
command: "sh",
|
|
21543
|
+
args: ["-c", custom.command],
|
|
21544
|
+
cwd,
|
|
21545
|
+
env: {
|
|
21546
|
+
...commandEnv,
|
|
21547
|
+
...credEnv,
|
|
21548
|
+
...custom.env
|
|
21549
|
+
}
|
|
21550
|
+
}, "custom ios build");
|
|
21551
|
+
const artifactPath = yield* findArtifactByGlob({
|
|
21552
|
+
baseDir: cwd,
|
|
21553
|
+
pattern: custom.artifactPath,
|
|
21554
|
+
minMtimeMs: buildStartMs
|
|
21555
|
+
});
|
|
21556
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
21557
|
+
return {
|
|
21558
|
+
artifactPath,
|
|
21559
|
+
byteSize,
|
|
21560
|
+
sha256
|
|
21561
|
+
};
|
|
21562
|
+
});
|
|
21563
|
+
const runIosBuild = (input) => {
|
|
21564
|
+
if (input.strategy === "custom") return runIosCustom(input);
|
|
21565
|
+
return input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
21566
|
+
};
|
|
21378
21567
|
|
|
21379
21568
|
//#endregion
|
|
21380
21569
|
//#region src/commands/build/reserve-and-upload.ts
|
|
21381
21570
|
const buildReserveCommon = (input) => ({
|
|
21382
21571
|
projectId: input.projectId,
|
|
21383
21572
|
profile: input.profileName,
|
|
21384
|
-
runtimeVersion: input.runtimeVersion,
|
|
21385
21573
|
bundleId: input.bundleId,
|
|
21386
21574
|
sha256: input.sha256,
|
|
21387
21575
|
byteSize: input.byteSize,
|
|
21388
21576
|
gitDirty: input.gitContext.dirty,
|
|
21389
21577
|
...compact({
|
|
21578
|
+
runtimeVersion: input.runtimeVersion,
|
|
21390
21579
|
appVersion: input.appVersion,
|
|
21391
21580
|
buildNumber: input.buildNumber,
|
|
21392
21581
|
gitRef: input.gitContext.ref,
|
|
@@ -21462,7 +21651,7 @@ const bumpVersionCode = (current) => Effect.gen(function* () {
|
|
|
21462
21651
|
if (!Number.isInteger(value) || value < 0) return yield* new BuildProfileError({ message: `Cannot autoIncrement android.versionCode: current value ${String(value)} is not a non-negative integer.` });
|
|
21463
21652
|
return value + 1;
|
|
21464
21653
|
});
|
|
21465
|
-
const SEMVER_PATCH = /^(
|
|
21654
|
+
const SEMVER_PATCH = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?<suffix>.*)$/u;
|
|
21466
21655
|
const bumpVersion = (current) => Effect.gen(function* () {
|
|
21467
21656
|
if (current === void 0) return yield* new BuildProfileError({ message: "Cannot autoIncrement version: no `version` field set in Expo config." });
|
|
21468
21657
|
const match = SEMVER_PATCH.exec(current);
|
|
@@ -21543,20 +21732,21 @@ const stripExtends = (profile) => {
|
|
|
21543
21732
|
};
|
|
21544
21733
|
const resolveExtendsChain = (params) => Effect.gen(function* () {
|
|
21545
21734
|
const { profiles, profileName, label, maxDepth, makeError } = params;
|
|
21735
|
+
const sourceLabel = params.sourceLabel ?? "eas.json";
|
|
21546
21736
|
const noun = label === "build" ? "Build" : "Submit";
|
|
21547
21737
|
const chain = [];
|
|
21548
21738
|
const visited = /* @__PURE__ */ new Set();
|
|
21549
21739
|
let current = profileName;
|
|
21550
21740
|
let depth = 0;
|
|
21551
21741
|
while (current !== void 0) {
|
|
21552
|
-
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in
|
|
21742
|
+
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in ${sourceLabel} ${label}.${profileName} extends chain at "${current}".`));
|
|
21553
21743
|
visited.add(current);
|
|
21554
21744
|
const profile = profiles[current];
|
|
21555
|
-
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in
|
|
21745
|
+
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in ${sourceLabel}.` : `${noun} profile "${profileName}" extends missing profile "${current}".`));
|
|
21556
21746
|
chain.unshift(profile);
|
|
21557
21747
|
current = profile.extends;
|
|
21558
21748
|
depth += 1;
|
|
21559
|
-
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in
|
|
21749
|
+
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in ${sourceLabel} ${label}.${profileName}.`));
|
|
21560
21750
|
}
|
|
21561
21751
|
return chain;
|
|
21562
21752
|
});
|
|
@@ -21623,13 +21813,14 @@ const mergeSubmitProfile = (base, overlay) => {
|
|
|
21623
21813
|
android
|
|
21624
21814
|
});
|
|
21625
21815
|
};
|
|
21626
|
-
const resolveEasSubmitProfile = (profiles, profileName) => Effect.gen(function* () {
|
|
21627
|
-
if (!profiles) return yield* new BuildProfileError({ message:
|
|
21816
|
+
const resolveEasSubmitProfile = (profiles, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21817
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "submit" section. Add at least one submit profile.` });
|
|
21628
21818
|
return stripExtends((yield* resolveExtendsChain({
|
|
21629
21819
|
profiles,
|
|
21630
21820
|
profileName,
|
|
21631
21821
|
label: "submit",
|
|
21632
21822
|
maxDepth: MAX_SUBMIT_EXTENDS_DEPTH,
|
|
21823
|
+
sourceLabel,
|
|
21633
21824
|
makeError: (message) => new BuildProfileError({ message })
|
|
21634
21825
|
})).reduce((acc, next, index) => index === 0 ? next : mergeSubmitProfile(acc, next), {}));
|
|
21635
21826
|
});
|
|
@@ -21696,7 +21887,13 @@ const parseIosProfile = (raw) => {
|
|
|
21696
21887
|
scheme: asStringValue(record["scheme"]),
|
|
21697
21888
|
simulator: asBooleanValue(record["simulator"]),
|
|
21698
21889
|
enterpriseProvisioning: asEnterpriseProvisioning(record["enterpriseProvisioning"]),
|
|
21699
|
-
autoIncrement: asIosAutoIncrement(record["autoIncrement"])
|
|
21890
|
+
autoIncrement: asIosAutoIncrement(record["autoIncrement"]),
|
|
21891
|
+
workspace: asStringValue(record["workspace"]),
|
|
21892
|
+
project: asStringValue(record["project"]),
|
|
21893
|
+
podInstall: asBooleanValue(record["podInstall"]),
|
|
21894
|
+
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
21895
|
+
version: asStringValue(record["version"]),
|
|
21896
|
+
buildNumber: asStringValue(record["buildNumber"])
|
|
21700
21897
|
});
|
|
21701
21898
|
};
|
|
21702
21899
|
const parseAndroidProfile = (raw) => {
|
|
@@ -21708,9 +21905,35 @@ const parseAndroidProfile = (raw) => {
|
|
|
21708
21905
|
gradleCommand: asStringValue(record["gradleCommand"]),
|
|
21709
21906
|
format: asAndroidFormat(record["format"]),
|
|
21710
21907
|
distribution: asAndroidDistribution(record["distribution"]),
|
|
21711
|
-
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"])
|
|
21908
|
+
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"]),
|
|
21909
|
+
module: asStringValue(record["module"]),
|
|
21910
|
+
gradleTask: asStringValue(record["gradleTask"]),
|
|
21911
|
+
applicationId: asStringValue(record["applicationId"]),
|
|
21912
|
+
version: asStringValue(record["version"]),
|
|
21913
|
+
versionCode: asStringValue(record["versionCode"])
|
|
21914
|
+
});
|
|
21915
|
+
};
|
|
21916
|
+
const parseCustomCommandSpec = (raw) => {
|
|
21917
|
+
const record = asRecord(raw);
|
|
21918
|
+
if (!record) return;
|
|
21919
|
+
const command = asStringValue(record["command"]);
|
|
21920
|
+
if (command === void 0) return;
|
|
21921
|
+
return compact({
|
|
21922
|
+
command,
|
|
21923
|
+
cwd: asStringValue(record["cwd"]),
|
|
21924
|
+
env: asEnv(record["env"]),
|
|
21925
|
+
artifactPath: asStringValue(record["artifactPath"])
|
|
21712
21926
|
});
|
|
21713
21927
|
};
|
|
21928
|
+
const parseCustomCommandProfile = (raw) => {
|
|
21929
|
+
const record = asRecord(raw);
|
|
21930
|
+
if (!record) return;
|
|
21931
|
+
const result = compact({
|
|
21932
|
+
ios: parseCustomCommandSpec(record["ios"]),
|
|
21933
|
+
android: parseCustomCommandSpec(record["android"])
|
|
21934
|
+
});
|
|
21935
|
+
return Object.keys(result).length === 0 ? void 0 : result;
|
|
21936
|
+
};
|
|
21714
21937
|
const parseBuildProfile = (raw) => {
|
|
21715
21938
|
const record = asRecord(raw);
|
|
21716
21939
|
if (!record) return;
|
|
@@ -21725,15 +21948,16 @@ const parseBuildProfile = (raw) => {
|
|
|
21725
21948
|
android: parseAndroidProfile(record["android"]),
|
|
21726
21949
|
credentialsSource: asCredentialsSource(record["credentialsSource"]),
|
|
21727
21950
|
autoIncrement: asAutoIncrement(record["autoIncrement"]),
|
|
21728
|
-
withoutCredentials: asBooleanValue(record["withoutCredentials"])
|
|
21951
|
+
withoutCredentials: asBooleanValue(record["withoutCredentials"]),
|
|
21952
|
+
custom: parseCustomCommandProfile(record["custom"])
|
|
21729
21953
|
});
|
|
21730
21954
|
};
|
|
21731
|
-
|
|
21732
|
-
|
|
21733
|
-
|
|
21734
|
-
|
|
21735
|
-
|
|
21736
|
-
|
|
21955
|
+
/**
|
|
21956
|
+
* Parse an already-decoded JSON object into an {@link EasConfig}. Shared by the
|
|
21957
|
+
* `eas.json` reader and the `better-update.json` build-config reader — both hold
|
|
21958
|
+
* the same `build`/`submit`/`cli` shape, only the source file differs.
|
|
21959
|
+
*/
|
|
21960
|
+
const parseConfigFromRecord = (root) => {
|
|
21737
21961
|
const buildRecord = asRecord(root["build"]);
|
|
21738
21962
|
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
21739
21963
|
const profiles = {};
|
|
@@ -21752,6 +21976,14 @@ const parseEasConfig = (text) => Effect.gen(function* () {
|
|
|
21752
21976
|
build: profiles,
|
|
21753
21977
|
...Object.keys(submit).length === 0 ? {} : { submit }
|
|
21754
21978
|
};
|
|
21979
|
+
};
|
|
21980
|
+
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
21981
|
+
const root = asRecord(yield* Effect.try({
|
|
21982
|
+
try: () => JSON.parse(text),
|
|
21983
|
+
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
21984
|
+
}));
|
|
21985
|
+
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
21986
|
+
return parseConfigFromRecord(root);
|
|
21755
21987
|
});
|
|
21756
21988
|
const parseCli = (raw) => {
|
|
21757
21989
|
const record = asRecord(raw);
|
|
@@ -21766,10 +21998,15 @@ const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
|
21766
21998
|
const filePath = yield* easJsonPath(projectRoot);
|
|
21767
21999
|
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.catchAll((cause) => Effect.fail(new BuildProfileError({ message: cause._tag === "SystemError" && cause.reason === "NotFound" ? `No eas.json found at ${filePath}. Create one with a "build" section.` : `Failed to read eas.json: ${cause.message}` })))));
|
|
21768
22000
|
});
|
|
22001
|
+
const mergeCustom = (base, overlay) => {
|
|
22002
|
+
const merged = shallowMerge(base, overlay);
|
|
22003
|
+
return merged === void 0 || Object.keys(merged).length === 0 ? void 0 : merged;
|
|
22004
|
+
};
|
|
21769
22005
|
const mergeProfile = (base, overlay) => {
|
|
21770
22006
|
const ios = shallowMerge(base.ios, overlay.ios);
|
|
21771
22007
|
const android = shallowMerge(base.android, overlay.android);
|
|
21772
22008
|
const env = shallowMerge(base.env, overlay.env);
|
|
22009
|
+
const custom = mergeCustom(base.custom, overlay.custom);
|
|
21773
22010
|
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
21774
22011
|
const distribution = overlay.distribution ?? base.distribution;
|
|
21775
22012
|
const channel = overlay.channel ?? base.channel;
|
|
@@ -21788,21 +22025,41 @@ const mergeProfile = (base, overlay) => {
|
|
|
21788
22025
|
android,
|
|
21789
22026
|
credentialsSource,
|
|
21790
22027
|
autoIncrement,
|
|
21791
|
-
withoutCredentials
|
|
22028
|
+
withoutCredentials,
|
|
22029
|
+
custom
|
|
21792
22030
|
});
|
|
21793
22031
|
};
|
|
21794
|
-
const resolveEasBuildProfile = (config, profileName) => Effect.gen(function* () {
|
|
22032
|
+
const resolveEasBuildProfile = (config, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21795
22033
|
const profiles = config.build;
|
|
21796
|
-
if (!profiles) return yield* new BuildProfileError({ message:
|
|
22034
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "build" section. Add at least one profile.` });
|
|
21797
22035
|
return stripExtends((yield* resolveExtendsChain({
|
|
21798
22036
|
profiles,
|
|
21799
22037
|
profileName,
|
|
21800
22038
|
label: "build",
|
|
21801
22039
|
maxDepth: MAX_EXTENDS_DEPTH,
|
|
22040
|
+
sourceLabel,
|
|
21802
22041
|
makeError: (message) => new BuildProfileError({ message })
|
|
21803
22042
|
})).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
21804
22043
|
});
|
|
21805
22044
|
|
|
22045
|
+
//#endregion
|
|
22046
|
+
//#region src/lib/better-update-build-config.ts
|
|
22047
|
+
/** Label used in profile-resolution error copy when config comes from this file. */
|
|
22048
|
+
const BETTER_UPDATE_SOURCE_LABEL = "better-update.json";
|
|
22049
|
+
/**
|
|
22050
|
+
* Read the `build`/`submit`/`cli` config from `better-update.json`. Returns an
|
|
22051
|
+
* empty config (no `build` key) when the file is absent or carries no build
|
|
22052
|
+
* section. Shares the parser with {@link file://./eas-config.ts}; only the
|
|
22053
|
+
* source file and the error `sourceLabel` differ.
|
|
22054
|
+
*/
|
|
22055
|
+
const readBuildConfig = (projectRoot) => readBetterUpdateConfig(projectRoot).pipe(Effect.map((config) => config === void 0 ? {} : parseConfigFromRecord(config)));
|
|
22056
|
+
/** List available build-profile names declared in `better-update.json`. */
|
|
22057
|
+
const listBuildProfileNames = (projectRoot) => readBuildConfig(projectRoot).pipe(Effect.map((config) => Object.keys(config.build ?? {})));
|
|
22058
|
+
/** Resolve a submit profile from `better-update.json`'s `submit` section. */
|
|
22059
|
+
const readSubmitProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
22060
|
+
return yield* resolveEasSubmitProfile((yield* readBuildConfig(projectRoot)).submit, profileName, BETTER_UPDATE_SOURCE_LABEL);
|
|
22061
|
+
});
|
|
22062
|
+
|
|
21806
22063
|
//#endregion
|
|
21807
22064
|
//#region src/lib/build-profile.ts
|
|
21808
22065
|
const deriveIosDistribution = (eas) => {
|
|
@@ -21849,12 +22106,22 @@ const toIosProfile = (eas) => {
|
|
|
21849
22106
|
if (!distribution) return;
|
|
21850
22107
|
const ios = eas.ios ?? {};
|
|
21851
22108
|
const autoIncrement = resolveIosAutoIncrement(eas);
|
|
22109
|
+
const buildConfiguration = ios.buildConfiguration ?? (eas.developmentClient === true ? "Debug" : void 0);
|
|
22110
|
+
const metaOverride = compact({
|
|
22111
|
+
bundleIdentifier: ios.bundleIdentifier,
|
|
22112
|
+
version: ios.version,
|
|
22113
|
+
buildNumber: ios.buildNumber
|
|
22114
|
+
});
|
|
21852
22115
|
return compact({
|
|
21853
22116
|
distribution,
|
|
21854
|
-
buildConfiguration
|
|
22117
|
+
buildConfiguration,
|
|
21855
22118
|
scheme: ios.scheme,
|
|
21856
22119
|
simulator: ios.simulator,
|
|
21857
|
-
autoIncrement
|
|
22120
|
+
autoIncrement,
|
|
22121
|
+
workspace: ios.workspace,
|
|
22122
|
+
project: ios.project,
|
|
22123
|
+
podInstall: ios.podInstall,
|
|
22124
|
+
metaOverride: Object.keys(metaOverride).length === 0 ? void 0 : metaOverride
|
|
21858
22125
|
});
|
|
21859
22126
|
};
|
|
21860
22127
|
const toAndroidProfile = (eas) => {
|
|
@@ -21864,16 +22131,25 @@ const toAndroidProfile = (eas) => {
|
|
|
21864
22131
|
const android = eas.android ?? {};
|
|
21865
22132
|
const distribution = deriveAndroidDistribution(eas, format);
|
|
21866
22133
|
const autoIncrement = resolveAndroidAutoIncrement(eas);
|
|
22134
|
+
const buildType = android.buildType ?? (eas.developmentClient === true ? "debug" : void 0);
|
|
22135
|
+
const metaOverride = compact({
|
|
22136
|
+
applicationId: android.applicationId,
|
|
22137
|
+
version: android.version,
|
|
22138
|
+
versionCode: android.versionCode
|
|
22139
|
+
});
|
|
21867
22140
|
return compact({
|
|
21868
22141
|
format,
|
|
21869
22142
|
distribution,
|
|
21870
|
-
buildType
|
|
22143
|
+
buildType,
|
|
21871
22144
|
flavor: android.flavor,
|
|
21872
22145
|
gradleCommand: android.gradleCommand,
|
|
21873
|
-
autoIncrement
|
|
22146
|
+
autoIncrement,
|
|
22147
|
+
module: android.module,
|
|
22148
|
+
gradleTask: android.gradleTask,
|
|
22149
|
+
metaOverride: Object.keys(metaOverride).length === 0 ? void 0 : metaOverride
|
|
21874
22150
|
});
|
|
21875
22151
|
};
|
|
21876
|
-
const
|
|
22152
|
+
const fromGenericProfile = (eas, profileName) => {
|
|
21877
22153
|
const ios = toIosProfile(eas);
|
|
21878
22154
|
const android = toAndroidProfile(eas);
|
|
21879
22155
|
return compact({
|
|
@@ -21885,11 +22161,13 @@ const fromEasProfile = (eas, profileName) => {
|
|
|
21885
22161
|
android,
|
|
21886
22162
|
credentialsSource: eas.credentialsSource,
|
|
21887
22163
|
developmentClient: eas.developmentClient,
|
|
21888
|
-
withoutCredentials: eas.withoutCredentials
|
|
22164
|
+
withoutCredentials: eas.withoutCredentials,
|
|
22165
|
+
customCommand: eas.custom
|
|
21889
22166
|
});
|
|
21890
22167
|
};
|
|
22168
|
+
/** Resolve a build profile from `better-update.json`'s `build` section. */
|
|
21891
22169
|
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
21892
|
-
return
|
|
22170
|
+
return fromGenericProfile(yield* resolveEasBuildProfile(yield* readBuildConfig(projectRoot), profileName, "better-update.json"), profileName);
|
|
21893
22171
|
});
|
|
21894
22172
|
const readRuntimeVersionMeta = (config, platform) => ({
|
|
21895
22173
|
platform,
|
|
@@ -21899,6 +22177,17 @@ const readRuntimeVersionMeta = (config, platform) => ({
|
|
|
21899
22177
|
rawRuntimeVersion: extractRawRuntimeVersion(config, platform)
|
|
21900
22178
|
});
|
|
21901
22179
|
|
|
22180
|
+
//#endregion
|
|
22181
|
+
//#region src/lib/build-strategy.ts
|
|
22182
|
+
const resolveAndroidStrategy = (profile, projectType) => {
|
|
22183
|
+
if (profile.customCommand?.android !== void 0) return "custom";
|
|
22184
|
+
return projectType === "expo" ? "expo" : "gradle";
|
|
22185
|
+
};
|
|
22186
|
+
const resolveIosStrategy = (profile, projectType) => {
|
|
22187
|
+
if (profile.customCommand?.ios !== void 0) return "custom";
|
|
22188
|
+
return projectType === "expo" ? "expo" : "xcode";
|
|
22189
|
+
};
|
|
22190
|
+
|
|
21902
22191
|
//#endregion
|
|
21903
22192
|
//#region src/lib/clear-cache.ts
|
|
21904
22193
|
/**
|
|
@@ -21926,6 +22215,64 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
|
|
|
21926
22215
|
if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
|
|
21927
22216
|
});
|
|
21928
22217
|
|
|
22218
|
+
//#endregion
|
|
22219
|
+
//#region src/lib/detect-project-type.ts
|
|
22220
|
+
const PROJECT_TYPES = [
|
|
22221
|
+
"expo",
|
|
22222
|
+
"bare",
|
|
22223
|
+
"kmp",
|
|
22224
|
+
"native",
|
|
22225
|
+
"custom"
|
|
22226
|
+
];
|
|
22227
|
+
/** Narrow an arbitrary `projectType` override (e.g. from better-update.json) to a valid value. */
|
|
22228
|
+
const asProjectType = (raw) => PROJECT_TYPES.find((type) => type === raw);
|
|
22229
|
+
const exists = (filePath) => Effect.gen(function* () {
|
|
22230
|
+
return yield* (yield* FileSystem.FileSystem).exists(filePath).pipe(Effect.orElseSucceed(() => false));
|
|
22231
|
+
});
|
|
22232
|
+
const readText = (filePath) => Effect.gen(function* () {
|
|
22233
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));
|
|
22234
|
+
});
|
|
22235
|
+
const hasExpoDependency = (projectRoot) => Effect.gen(function* () {
|
|
22236
|
+
const text = yield* readText(path.join(projectRoot, "package.json"));
|
|
22237
|
+
if (text.length === 0) return false;
|
|
22238
|
+
const pkg = asRecord(yield* Effect.try(() => JSON.parse(text)).pipe(Effect.orElseSucceed(() => void 0)));
|
|
22239
|
+
const deps = asRecord(pkg?.["dependencies"]);
|
|
22240
|
+
const devDeps = asRecord(pkg?.["devDependencies"]);
|
|
22241
|
+
return deps?.["expo"] !== void 0 || devDeps?.["expo"] !== void 0;
|
|
22242
|
+
});
|
|
22243
|
+
const hasAnyExpoConfigFile = (projectRoot) => Effect.gen(function* () {
|
|
22244
|
+
for (const name of [
|
|
22245
|
+
"app.json",
|
|
22246
|
+
"app.config.js",
|
|
22247
|
+
"app.config.ts"
|
|
22248
|
+
]) if (yield* exists(path.join(projectRoot, name))) return true;
|
|
22249
|
+
return false;
|
|
22250
|
+
});
|
|
22251
|
+
const looksKmp = (projectRoot) => Effect.gen(function* () {
|
|
22252
|
+
if (yield* exists(path.join(projectRoot, "composeApp"))) return true;
|
|
22253
|
+
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
22254
|
+
const text = yield* readText(path.join(projectRoot, name));
|
|
22255
|
+
if (text.includes("composeApp") || text.includes(":shared")) return true;
|
|
22256
|
+
}
|
|
22257
|
+
return yield* exists(path.join(projectRoot, "android", "app", "build.gradle.kts"));
|
|
22258
|
+
});
|
|
22259
|
+
/**
|
|
22260
|
+
* Resolve a project's build-system family. An explicit override always wins;
|
|
22261
|
+
* otherwise the filesystem shape is inspected. `custom` is never auto-detected —
|
|
22262
|
+
* it is intent expressed via override or a profile `custom` block.
|
|
22263
|
+
*/
|
|
22264
|
+
const detectProjectType = (params) => Effect.gen(function* () {
|
|
22265
|
+
if (params.override !== void 0) return params.override;
|
|
22266
|
+
const { projectRoot } = params;
|
|
22267
|
+
if (isExpoConfigInstalled() && ((yield* hasExpoDependency(projectRoot)) || (yield* hasAnyExpoConfigFile(projectRoot)))) return "expo";
|
|
22268
|
+
if (yield* looksKmp(projectRoot)) return "kmp";
|
|
22269
|
+
const hasAndroid = yield* exists(path.join(projectRoot, "android"));
|
|
22270
|
+
const hasIos = yield* exists(path.join(projectRoot, "ios"));
|
|
22271
|
+
const hasPackageJson = yield* exists(path.join(projectRoot, "package.json"));
|
|
22272
|
+
if (hasAndroid && hasIos && hasPackageJson) return "bare";
|
|
22273
|
+
return "native";
|
|
22274
|
+
});
|
|
22275
|
+
|
|
21929
22276
|
//#endregion
|
|
21930
22277
|
//#region src/lib/dev-client-check.ts
|
|
21931
22278
|
const readDeps = (filePath) => Effect.gen(function* () {
|
|
@@ -22291,6 +22638,28 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
|
|
|
22291
22638
|
label: entry
|
|
22292
22639
|
})));
|
|
22293
22640
|
});
|
|
22641
|
+
/**
|
|
22642
|
+
* Resolve a build platform for non-Expo projects from an explicit flag, or by
|
|
22643
|
+
* intersecting the profile's declared platform sections with the native dirs
|
|
22644
|
+
* present on disk. Prompts when both remain; fails when ambiguous and prompts
|
|
22645
|
+
* are disallowed.
|
|
22646
|
+
*/
|
|
22647
|
+
const detectPlatformGeneric = (explicit, context) => Effect.gen(function* () {
|
|
22648
|
+
if (explicit !== void 0) return explicit;
|
|
22649
|
+
const candidates = [];
|
|
22650
|
+
const wantsIos = context.profile.ios !== void 0 || context.profile.customCommand?.ios !== void 0;
|
|
22651
|
+
const wantsAndroid = context.profile.android !== void 0 || context.profile.customCommand?.android !== void 0;
|
|
22652
|
+
if (wantsIos && (context.hasIosDir || context.profile.customCommand?.ios !== void 0)) candidates.push("ios");
|
|
22653
|
+
if (wantsAndroid && (context.hasAndroidDir || context.profile.customCommand?.android !== void 0)) candidates.push("android");
|
|
22654
|
+
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to the build profile in better-update.json, or pass --platform." });
|
|
22655
|
+
const [only] = candidates;
|
|
22656
|
+
if (candidates.length === 1 && only !== void 0) return only;
|
|
22657
|
+
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms available (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
22658
|
+
return yield* promptSelect("Which platform to build?", candidates.map((entry) => ({
|
|
22659
|
+
value: entry,
|
|
22660
|
+
label: entry
|
|
22661
|
+
})));
|
|
22662
|
+
});
|
|
22294
22663
|
|
|
22295
22664
|
//#endregion
|
|
22296
22665
|
//#region src/lib/project-staging.ts
|
|
@@ -22303,24 +22672,30 @@ const LOCKFILES = [
|
|
|
22303
22672
|
["package-lock.json", "npm"]
|
|
22304
22673
|
];
|
|
22305
22674
|
/**
|
|
22306
|
-
*
|
|
22307
|
-
*
|
|
22675
|
+
* Generated native build outputs / dependency dirs that must never be copied —
|
|
22676
|
+
* they are regenerated in staging (Pods via `pod install`, build/ via gradle).
|
|
22308
22677
|
*/
|
|
22309
|
-
const
|
|
22310
|
-
"node_modules",
|
|
22311
|
-
".git",
|
|
22678
|
+
const NATIVE_BUILD_OUTPUTS = [
|
|
22312
22679
|
"ios/build",
|
|
22313
22680
|
"ios/Pods",
|
|
22314
22681
|
"ios/DerivedData",
|
|
22315
22682
|
"android/build",
|
|
22316
22683
|
"android/app/build",
|
|
22317
22684
|
"android/.gradle",
|
|
22318
|
-
"android/.kotlin"
|
|
22685
|
+
"android/.kotlin"
|
|
22686
|
+
];
|
|
22687
|
+
/**
|
|
22688
|
+
* Paths never copied into staging — covers generated native build outputs and
|
|
22689
|
+
* dependency dirs that must be reinstalled fresh in staging.
|
|
22690
|
+
*/
|
|
22691
|
+
const ALWAYS_IGNORE = [...[
|
|
22692
|
+
"node_modules",
|
|
22693
|
+
".git",
|
|
22319
22694
|
".expo",
|
|
22320
22695
|
".gradle",
|
|
22321
22696
|
".turbo",
|
|
22322
22697
|
"dist"
|
|
22323
|
-
];
|
|
22698
|
+
], ...NATIVE_BUILD_OUTPUTS];
|
|
22324
22699
|
const findLockfile = (fs, dir) => Effect.gen(function* () {
|
|
22325
22700
|
for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.catchAll(() => Effect.succeed(false)))) return pm;
|
|
22326
22701
|
});
|
|
@@ -22348,8 +22723,12 @@ const detectWorkspaceRoot = (cwd) => walkUpForLockfile(cwd, cwd);
|
|
|
22348
22723
|
* Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES
|
|
22349
22724
|
* `.gitignore` when present (matches EAS semantics); otherwise `.gitignore`
|
|
22350
22725
|
* is layered on top of the always-ignore baseline.
|
|
22726
|
+
*
|
|
22727
|
+
* When `includeNativeSource` is set, the native source dirs are re-included
|
|
22728
|
+
* after the ignore files are applied, then their build outputs re-excluded, so
|
|
22729
|
+
* a committed `ios/`/`android/` reaches staging intact.
|
|
22351
22730
|
*/
|
|
22352
|
-
const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
|
|
22731
|
+
const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function* () {
|
|
22353
22732
|
const fs = yield* FileSystem.FileSystem;
|
|
22354
22733
|
const ig = ignore();
|
|
22355
22734
|
ig.add([...ALWAYS_IGNORE]);
|
|
@@ -22357,12 +22736,17 @@ const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
|
|
|
22357
22736
|
if (yield* fs.exists(easignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
|
|
22358
22737
|
const content = yield* fs.readFileString(easignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
22359
22738
|
ig.add(content);
|
|
22360
|
-
|
|
22739
|
+
} else {
|
|
22740
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
22741
|
+
if (yield* fs.exists(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
|
|
22742
|
+
const content = yield* fs.readFileString(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
22743
|
+
ig.add(content);
|
|
22744
|
+
}
|
|
22361
22745
|
}
|
|
22362
|
-
|
|
22363
|
-
|
|
22364
|
-
|
|
22365
|
-
ig.add(
|
|
22746
|
+
if (options.includeNativeSource === true) {
|
|
22747
|
+
const base = options.appRelPath === void 0 || options.appRelPath === "" ? "" : `${options.appRelPath}/`;
|
|
22748
|
+
ig.add([`!${base}android`, `!${base}ios`]);
|
|
22749
|
+
ig.add(NATIVE_BUILD_OUTPUTS.map((entry) => `${base}${entry}`));
|
|
22366
22750
|
}
|
|
22367
22751
|
return ig;
|
|
22368
22752
|
});
|
|
@@ -22410,6 +22794,7 @@ const runInstall = (params) => runStep({
|
|
|
22410
22794
|
* regardless of what `expo prebuild`, `pod install`, or `gradlew` write.
|
|
22411
22795
|
*/
|
|
22412
22796
|
const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
22797
|
+
const fs = yield* FileSystem.FileSystem;
|
|
22413
22798
|
const runtime = yield* CliRuntime;
|
|
22414
22799
|
const { workspaceRoot, packageManager } = yield* detectWorkspaceRoot(input.userCwd);
|
|
22415
22800
|
const relAppPath = path.relative(workspaceRoot, input.userCwd);
|
|
@@ -22419,14 +22804,18 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
|
22419
22804
|
yield* copyProjectTree({
|
|
22420
22805
|
source: workspaceRoot,
|
|
22421
22806
|
dest: stagingRoot,
|
|
22422
|
-
ig: yield* buildIgnoreInstance(workspaceRoot
|
|
22807
|
+
ig: yield* buildIgnoreInstance(workspaceRoot, {
|
|
22808
|
+
includeNativeSource: input.projectType !== void 0 && input.projectType !== "expo",
|
|
22809
|
+
appRelPath: relAppPath
|
|
22810
|
+
})
|
|
22423
22811
|
});
|
|
22424
22812
|
yield* initGitRepo(stagingRoot);
|
|
22425
|
-
yield* runInstall({
|
|
22813
|
+
if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.catchAll(() => Effect.succeed(false)))) yield* runInstall({
|
|
22426
22814
|
stagingRoot,
|
|
22427
22815
|
packageManager,
|
|
22428
22816
|
env: yield* runtime.commandEnvironment(input.envVars)
|
|
22429
22817
|
});
|
|
22818
|
+
else yield* printHuman("No package.json at the staging root — skipping dependency install.");
|
|
22430
22819
|
return {
|
|
22431
22820
|
stagingRoot,
|
|
22432
22821
|
projectRoot,
|
|
@@ -23026,7 +23415,7 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
|
23026
23415
|
});
|
|
23027
23416
|
|
|
23028
23417
|
//#endregion
|
|
23029
|
-
//#region src/application/build-
|
|
23418
|
+
//#region src/application/build-auto-submit.ts
|
|
23030
23419
|
const buildAutoSubmitIosConfig = (iosProfile, whatToTest) => {
|
|
23031
23420
|
if (iosProfile?.bundleIdentifier === void 0) return;
|
|
23032
23421
|
return compact({
|
|
@@ -23052,9 +23441,10 @@ const buildAutoSubmitAndroidConfig = (androidProfile) => {
|
|
|
23052
23441
|
rollout: androidProfile.rollout
|
|
23053
23442
|
});
|
|
23054
23443
|
};
|
|
23444
|
+
/** Submit a freshly-built artifact to the store using the profile's submit config. */
|
|
23055
23445
|
const runAutoSubmit = (input) => Effect.gen(function* () {
|
|
23056
23446
|
yield* printHuman(`\nAuto-submitting build ${input.buildId} (profile ${input.profileName})...`);
|
|
23057
|
-
const easProfile = yield*
|
|
23447
|
+
const easProfile = yield* readSubmitProfile(yield* (yield* CliRuntime).cwd, input.profileName);
|
|
23058
23448
|
const archiveUrl = (yield* input.api.builds.getInstallLink({ path: { id: input.buildId } })).artifactUrl;
|
|
23059
23449
|
const iosConfig = input.platform === "ios" ? buildAutoSubmitIosConfig(easProfile.ios, input.whatToTest) : void 0;
|
|
23060
23450
|
const androidConfig = input.platform === "android" ? buildAutoSubmitAndroidConfig(easProfile.android) : void 0;
|
|
@@ -23082,15 +23472,146 @@ const runAutoSubmit = (input) => Effect.gen(function* () {
|
|
|
23082
23472
|
}
|
|
23083
23473
|
yield* printHuman(`Submission final status: ${(yield* pollSubmissionUntilTerminal(input.api, submission.id)).status}`);
|
|
23084
23474
|
});
|
|
23475
|
+
|
|
23476
|
+
//#endregion
|
|
23477
|
+
//#region src/lib/ios-native-meta.ts
|
|
23478
|
+
const APPLICATION_PRODUCT_TYPE = "com.apple.product-type.application";
|
|
23479
|
+
/**
|
|
23480
|
+
* A build setting that is an unresolved reference (`$(MARKETING_VERSION)`) or a
|
|
23481
|
+
* variable interpolation tells us nothing concrete — treat it as absent so the
|
|
23482
|
+
* profile `metaOverride` can supply a real value.
|
|
23483
|
+
*/
|
|
23484
|
+
const concreteSetting = (raw) => {
|
|
23485
|
+
if (typeof raw === "number") return String(raw);
|
|
23486
|
+
if (typeof raw !== "string") return;
|
|
23487
|
+
const value = unquote$1(raw);
|
|
23488
|
+
return value.length === 0 || value.includes("$(") ? void 0 : value;
|
|
23489
|
+
};
|
|
23490
|
+
const findApplicationTarget = (project) => {
|
|
23491
|
+
const nativeTargets = project.pbxNativeTargetSection();
|
|
23492
|
+
for (const [uuid, entry] of Object.entries(nativeTargets)) {
|
|
23493
|
+
if (uuid.endsWith("_comment") || typeof entry === "string") continue;
|
|
23494
|
+
if (unquote$1(entry.productType) === APPLICATION_PRODUCT_TYPE) return entry;
|
|
23495
|
+
}
|
|
23496
|
+
};
|
|
23497
|
+
const configUuidForName = (project, target, configurationName) => {
|
|
23498
|
+
const configList = project.pbxXCConfigurationList()[target.buildConfigurationList];
|
|
23499
|
+
if (!configList || typeof configList === "string") return;
|
|
23500
|
+
const buildConfigSection = project.pbxXCBuildConfigurationSection();
|
|
23501
|
+
return configList.buildConfigurations.map((entry) => entry.value).find((uuid) => {
|
|
23502
|
+
const cfg = buildConfigSection[uuid];
|
|
23503
|
+
return cfg !== void 0 && typeof cfg !== "string" && unquote$1(cfg.name) === configurationName;
|
|
23504
|
+
});
|
|
23505
|
+
};
|
|
23506
|
+
/**
|
|
23507
|
+
* Read app metadata (bundle id, marketing version, build number) for the main
|
|
23508
|
+
* application target of the single `.xcodeproj` under `iosDir`, for a given build
|
|
23509
|
+
* configuration. Used for non-Expo (bare/native) projects where there is no
|
|
23510
|
+
* `app.json`. Missing or unresolved settings come back `undefined` so the caller
|
|
23511
|
+
* can fall back to profile `metaOverride`.
|
|
23512
|
+
*/
|
|
23513
|
+
const readIosNativeMeta = (params) => Effect.gen(function* () {
|
|
23514
|
+
const projectDir = yield* findXcodeProjectDir(params.iosDir);
|
|
23515
|
+
const project = yield* parseProject(path.join(projectDir, "project.pbxproj"));
|
|
23516
|
+
const target = findApplicationTarget(project);
|
|
23517
|
+
if (target === void 0) return {};
|
|
23518
|
+
const configUuid = configUuidForName(project, target, params.configurationName);
|
|
23519
|
+
if (configUuid === void 0) return {};
|
|
23520
|
+
const cfg = project.pbxXCBuildConfigurationSection()[configUuid];
|
|
23521
|
+
if (cfg === void 0 || typeof cfg === "string") return {};
|
|
23522
|
+
const settings = cfg.buildSettings;
|
|
23523
|
+
return compact({
|
|
23524
|
+
bundleId: concreteSetting(settings["PRODUCT_BUNDLE_IDENTIFIER"]),
|
|
23525
|
+
marketingVersion: concreteSetting(settings["MARKETING_VERSION"]),
|
|
23526
|
+
currentProjectVersion: concreteSetting(settings["CURRENT_PROJECT_VERSION"])
|
|
23527
|
+
});
|
|
23528
|
+
});
|
|
23529
|
+
|
|
23530
|
+
//#endregion
|
|
23531
|
+
//#region src/application/resolve-app-meta.ts
|
|
23532
|
+
const EMPTY = {
|
|
23533
|
+
bundleId: void 0,
|
|
23534
|
+
androidPackage: void 0,
|
|
23535
|
+
appVersion: void 0,
|
|
23536
|
+
buildNumber: void 0,
|
|
23537
|
+
rawRuntimeVersion: void 0
|
|
23538
|
+
};
|
|
23539
|
+
const warnIfMismatch = (label, override, native) => override !== void 0 && native !== void 0 && override !== native ? printWarn(`${label} override "${override}" differs from the native value "${native}". The better-update.json value will be used for build metadata.`) : Effect.void;
|
|
23540
|
+
const resolveAndroidMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23541
|
+
const gradle = yield* readGradleConfig(path.join(projectRoot, "android"));
|
|
23542
|
+
const override = profile.android?.metaOverride;
|
|
23543
|
+
yield* warnIfMismatch("android.applicationId", override?.applicationId, gradle?.applicationId);
|
|
23544
|
+
const androidPackage = override?.applicationId ?? gradle?.applicationId;
|
|
23545
|
+
if (androidPackage === void 0) return yield* new BuildProfileError({ message: "Could not determine the Android applicationId. Set android.applicationId under this build profile in better-update.json, or ensure android/app/build.gradle defines it." });
|
|
23546
|
+
const versionCode = override?.versionCode ?? (gradle?.versionCode === void 0 ? void 0 : String(gradle.versionCode));
|
|
23547
|
+
return {
|
|
23548
|
+
...EMPTY,
|
|
23549
|
+
androidPackage,
|
|
23550
|
+
appVersion: override?.version ?? gradle?.versionName,
|
|
23551
|
+
buildNumber: versionCode
|
|
23552
|
+
};
|
|
23553
|
+
});
|
|
23554
|
+
const resolveIosMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23555
|
+
const configurationName = profile.ios?.buildConfiguration ?? "Release";
|
|
23556
|
+
const native = yield* readIosNativeMeta({
|
|
23557
|
+
iosDir: path.join(projectRoot, "ios"),
|
|
23558
|
+
configurationName
|
|
23559
|
+
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
23560
|
+
const override = profile.ios?.metaOverride;
|
|
23561
|
+
yield* warnIfMismatch("ios.bundleIdentifier", override?.bundleIdentifier, native.bundleId);
|
|
23562
|
+
const bundleId = override?.bundleIdentifier ?? native.bundleId;
|
|
23563
|
+
if (bundleId === void 0) return yield* new BuildProfileError({ message: "Could not determine the iOS bundle identifier. Set ios.bundleIdentifier under this build profile in better-update.json, or ensure the Xcode project defines PRODUCT_BUNDLE_IDENTIFIER for the build configuration." });
|
|
23564
|
+
return {
|
|
23565
|
+
...EMPTY,
|
|
23566
|
+
bundleId,
|
|
23567
|
+
appVersion: override?.version ?? native.marketingVersion,
|
|
23568
|
+
buildNumber: override?.buildNumber ?? native.currentProjectVersion
|
|
23569
|
+
};
|
|
23570
|
+
});
|
|
23571
|
+
const overlayExpoOverride = (meta, platform, profile) => {
|
|
23572
|
+
if (platform === "ios") {
|
|
23573
|
+
const override = profile.ios?.metaOverride;
|
|
23574
|
+
return {
|
|
23575
|
+
...meta,
|
|
23576
|
+
bundleId: override?.bundleIdentifier ?? meta.bundleId,
|
|
23577
|
+
appVersion: override?.version ?? meta.appVersion,
|
|
23578
|
+
buildNumber: override?.buildNumber ?? meta.buildNumber
|
|
23579
|
+
};
|
|
23580
|
+
}
|
|
23581
|
+
const override = profile.android?.metaOverride;
|
|
23582
|
+
return {
|
|
23583
|
+
...meta,
|
|
23584
|
+
androidPackage: override?.applicationId ?? meta.androidPackage,
|
|
23585
|
+
appVersion: override?.version ?? meta.appVersion,
|
|
23586
|
+
buildNumber: override?.versionCode ?? meta.buildNumber
|
|
23587
|
+
};
|
|
23588
|
+
};
|
|
23589
|
+
/**
|
|
23590
|
+
* Resolve app metadata (bundle id / package, version, build number) in a
|
|
23591
|
+
* project-type-aware way. Expo reads from app.json; bare/native read from native
|
|
23592
|
+
* files (build.gradle / pbxproj); KMP/custom rely on profile `metaOverride`,
|
|
23593
|
+
* which also overrides native values when both are present.
|
|
23594
|
+
*/
|
|
23595
|
+
const resolveAppMeta = (params) => {
|
|
23596
|
+
if (params.projectType === "expo") {
|
|
23597
|
+
if (params.expoAppMeta === void 0) return Effect.fail(new BuildProfileError({ message: "Internal: missing Expo app metadata for expo project." }));
|
|
23598
|
+
return Effect.succeed(overlayExpoOverride(params.expoAppMeta, params.platform, params.profile));
|
|
23599
|
+
}
|
|
23600
|
+
return params.platform === "ios" ? resolveIosMeta(params.projectRoot, params.profile) : resolveAndroidMeta(params.projectRoot, params.profile);
|
|
23601
|
+
};
|
|
23602
|
+
|
|
23603
|
+
//#endregion
|
|
23604
|
+
//#region src/application/build-workflow.ts
|
|
23085
23605
|
const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
23086
23606
|
const { api, appMeta, envVars, options, profile, projectId, projectRoot, tempDir } = input;
|
|
23087
23607
|
if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
|
|
23088
23608
|
const iosProfile = profile.ios;
|
|
23089
23609
|
const iosBundleId = appMeta.bundleId;
|
|
23090
|
-
if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier
|
|
23610
|
+
if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing iOS bundle identifier (set ios.bundleIdentifier or your Expo config)." });
|
|
23611
|
+
const strategy = resolveIosStrategy(profile, input.projectType);
|
|
23091
23612
|
const isSimulator = iosProfile.simulator === true;
|
|
23092
23613
|
const credentialsSource = profile.credentialsSource ?? "remote";
|
|
23093
|
-
if (!isSimulator && credentialsSource === "remote") yield* ensureIosCredentials(api, {
|
|
23614
|
+
if (strategy !== "custom" && !isSimulator && credentialsSource === "remote") yield* ensureIosCredentials(api, {
|
|
23094
23615
|
projectId,
|
|
23095
23616
|
bundleIdentifier: iosBundleId,
|
|
23096
23617
|
distribution: iosProfile.distribution
|
|
@@ -23105,8 +23626,10 @@ const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23105
23626
|
envVars,
|
|
23106
23627
|
projectId,
|
|
23107
23628
|
credentialsSource,
|
|
23629
|
+
strategy,
|
|
23108
23630
|
rawOutput: options.rawOutput,
|
|
23109
|
-
freezeCredentials: options.freezeCredentials ?? false
|
|
23631
|
+
freezeCredentials: options.freezeCredentials ?? false,
|
|
23632
|
+
...compact({ customCommand: profile.customCommand?.ios })
|
|
23110
23633
|
}),
|
|
23111
23634
|
target: isSimulator ? {
|
|
23112
23635
|
platform: "ios",
|
|
@@ -23125,7 +23648,8 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23125
23648
|
if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
|
|
23126
23649
|
const androidProfile = profile.android;
|
|
23127
23650
|
const androidBundleId = appMeta.androidPackage;
|
|
23128
|
-
if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing android.
|
|
23651
|
+
if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing Android applicationId (set android.applicationId or your Expo config)." });
|
|
23652
|
+
const strategy = resolveAndroidStrategy(profile, input.projectType);
|
|
23129
23653
|
const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
|
|
23130
23654
|
yield* warnOnGradleMismatch(gradleConfig, androidBundleId);
|
|
23131
23655
|
const applicationIdentifier = gradleConfig?.applicationId ?? androidBundleId;
|
|
@@ -23146,7 +23670,9 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23146
23670
|
projectId,
|
|
23147
23671
|
credentialsSource,
|
|
23148
23672
|
profileName: profile.name,
|
|
23149
|
-
skipCredentials
|
|
23673
|
+
skipCredentials,
|
|
23674
|
+
strategy,
|
|
23675
|
+
...compact({ customCommand: profile.customCommand?.android })
|
|
23150
23676
|
}),
|
|
23151
23677
|
target: androidProfile.format === "aab" ? {
|
|
23152
23678
|
platform: "android",
|
|
@@ -23161,12 +23687,48 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23161
23687
|
};
|
|
23162
23688
|
});
|
|
23163
23689
|
const runPlatformBuild = (input) => input.platform === "ios" ? runIosPlatformBuild(input) : runAndroidPlatformBuild(input);
|
|
23690
|
+
const dirExists = (root, name) => Effect.gen(function* () {
|
|
23691
|
+
return yield* (yield* FileSystem.FileSystem).exists(path.join(root, name)).pipe(Effect.orElseSucceed(() => false));
|
|
23692
|
+
});
|
|
23693
|
+
/**
|
|
23694
|
+
* Expo metadata path: read app.json (with the env overlay so dynamic configs
|
|
23695
|
+
* resolve), apply autoIncrement to the user's tree, re-read, then derive the OTA
|
|
23696
|
+
* runtimeVersion. Mirrors the original managed flow.
|
|
23697
|
+
*/
|
|
23698
|
+
const resolveExpoBuildMeta = (params) => Effect.gen(function* () {
|
|
23699
|
+
const { userCwd, platform, profile, envVars } = params;
|
|
23700
|
+
yield* applyAutoIncrement({
|
|
23701
|
+
projectRoot: userCwd,
|
|
23702
|
+
platform,
|
|
23703
|
+
config: yield* readExpoConfig(userCwd, envVars),
|
|
23704
|
+
...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
|
|
23705
|
+
...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
|
|
23706
|
+
});
|
|
23707
|
+
const bumpedConfig = yield* readExpoConfig(userCwd, envVars);
|
|
23708
|
+
const appMeta = yield* resolveAppMeta({
|
|
23709
|
+
projectType: "expo",
|
|
23710
|
+
platform,
|
|
23711
|
+
projectRoot: userCwd,
|
|
23712
|
+
profile,
|
|
23713
|
+
expoAppMeta: yield* readAppMeta(bumpedConfig, platform)
|
|
23714
|
+
});
|
|
23715
|
+
return {
|
|
23716
|
+
appMeta,
|
|
23717
|
+
runtimeVersion: yield* resolveRuntimeVersion({
|
|
23718
|
+
raw: appMeta.rawRuntimeVersion,
|
|
23719
|
+
appVersion: appMeta.appVersion,
|
|
23720
|
+
projectRoot: userCwd,
|
|
23721
|
+
platform,
|
|
23722
|
+
buildNumber: appMeta.buildNumber,
|
|
23723
|
+
sdkVersion: bumpedConfig.sdkVersion
|
|
23724
|
+
})
|
|
23725
|
+
};
|
|
23726
|
+
});
|
|
23164
23727
|
const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
|
|
23165
|
-
const
|
|
23166
|
-
const available = Object.keys(easConfig.build ?? {});
|
|
23728
|
+
const available = yield* listBuildProfileNames(projectRoot);
|
|
23167
23729
|
if (available.includes(requested)) return requested;
|
|
23168
23730
|
if (!(yield* InteractiveMode).allow || available.length === 0) return requested;
|
|
23169
|
-
yield* printHuman(`Build profile "${requested}" not found in
|
|
23731
|
+
yield* printHuman(`Build profile "${requested}" not found in better-update.json.`);
|
|
23170
23732
|
return yield* promptSelect("Pick a build profile:", available.map((name) => ({
|
|
23171
23733
|
value: name,
|
|
23172
23734
|
label: name
|
|
@@ -23180,11 +23742,19 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23180
23742
|
allowDirty: options.allowDirty ?? false,
|
|
23181
23743
|
label: "build"
|
|
23182
23744
|
});
|
|
23183
|
-
const
|
|
23184
|
-
|
|
23185
|
-
|
|
23745
|
+
const projectType = yield* detectProjectType({
|
|
23746
|
+
projectRoot: userCwd,
|
|
23747
|
+
override: asProjectType((yield* readBetterUpdateConfig(userCwd))?.["projectType"])
|
|
23748
|
+
});
|
|
23749
|
+
const isExpo = projectType === "expo";
|
|
23750
|
+
const projectId = yield* readProjectId;
|
|
23186
23751
|
const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
|
|
23187
23752
|
if (profile.developmentClient === true) yield* warnIfDevClientMissing(userCwd);
|
|
23753
|
+
const platform = isExpo ? yield* detectPlatform(options.platform, yield* readExpoConfig(userCwd)) : yield* detectPlatformGeneric(options.platform, {
|
|
23754
|
+
profile,
|
|
23755
|
+
hasAndroidDir: yield* dirExists(userCwd, "android"),
|
|
23756
|
+
hasIosDir: yield* dirExists(userCwd, "ios")
|
|
23757
|
+
});
|
|
23188
23758
|
const envVars = {
|
|
23189
23759
|
...yield* pullEnvVars(api, {
|
|
23190
23760
|
projectId,
|
|
@@ -23192,36 +23762,35 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23192
23762
|
}),
|
|
23193
23763
|
...profile.env
|
|
23194
23764
|
};
|
|
23195
|
-
yield*
|
|
23196
|
-
|
|
23197
|
-
platform,
|
|
23198
|
-
config: yield* readExpoConfig(userCwd, envVars),
|
|
23199
|
-
...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
|
|
23200
|
-
...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
|
|
23201
|
-
});
|
|
23202
|
-
const bumpedConfig = yield* readExpoConfig(userCwd, envVars);
|
|
23203
|
-
const appMeta = yield* readAppMeta(bumpedConfig, platform);
|
|
23204
|
-
const runtimeVersion = yield* resolveRuntimeVersion({
|
|
23205
|
-
raw: appMeta.rawRuntimeVersion,
|
|
23206
|
-
appVersion: appMeta.appVersion,
|
|
23207
|
-
projectRoot: userCwd,
|
|
23765
|
+
const { appMeta, runtimeVersion } = isExpo ? yield* resolveExpoBuildMeta({
|
|
23766
|
+
userCwd,
|
|
23208
23767
|
platform,
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
})
|
|
23768
|
+
profile,
|
|
23769
|
+
envVars
|
|
23770
|
+
}) : {
|
|
23771
|
+
appMeta: yield* resolveAppMeta({
|
|
23772
|
+
projectType,
|
|
23773
|
+
platform,
|
|
23774
|
+
projectRoot: userCwd,
|
|
23775
|
+
profile
|
|
23776
|
+
}),
|
|
23777
|
+
runtimeVersion: void 0
|
|
23778
|
+
};
|
|
23212
23779
|
if (options.clearCache) yield* clearBuildCaches(userCwd);
|
|
23213
23780
|
const tempDir = yield* acquireBuildTempDir;
|
|
23214
23781
|
const staging = yield* prepareStagingProject({
|
|
23215
23782
|
userCwd,
|
|
23216
23783
|
tempDir,
|
|
23217
|
-
envVars
|
|
23784
|
+
envVars,
|
|
23785
|
+
projectType
|
|
23218
23786
|
});
|
|
23219
|
-
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}" (runtimeVersion=${runtimeVersion})`);
|
|
23787
|
+
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}"${runtimeVersion === void 0 ? "" : ` (runtimeVersion=${runtimeVersion})`}`);
|
|
23220
23788
|
const { build, target, bundleId } = yield* runPlatformBuild({
|
|
23221
23789
|
api,
|
|
23222
23790
|
options,
|
|
23223
23791
|
platform,
|
|
23224
23792
|
profile,
|
|
23793
|
+
projectType,
|
|
23225
23794
|
appMeta,
|
|
23226
23795
|
envVars,
|
|
23227
23796
|
projectId,
|
|
@@ -23255,18 +23824,18 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23255
23824
|
commit: rawGitContext.commit,
|
|
23256
23825
|
dirty: rawGitContext.dirty
|
|
23257
23826
|
});
|
|
23258
|
-
const fingerprintHash = yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
23827
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
|
|
23259
23828
|
const result = yield* reserveAndUpload(api, {
|
|
23260
23829
|
target,
|
|
23261
23830
|
projectId,
|
|
23262
23831
|
profileName: profile.name,
|
|
23263
|
-
runtimeVersion,
|
|
23264
23832
|
bundleId,
|
|
23265
23833
|
gitContext,
|
|
23266
23834
|
artifactPath: build.artifactPath,
|
|
23267
23835
|
sha256: build.sha256,
|
|
23268
23836
|
byteSize: build.byteSize,
|
|
23269
23837
|
...compact({
|
|
23838
|
+
runtimeVersion,
|
|
23270
23839
|
appVersion: appMeta.appVersion,
|
|
23271
23840
|
buildNumber: appMeta.buildNumber,
|
|
23272
23841
|
message: options.message,
|
|
@@ -23279,7 +23848,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23279
23848
|
["Status", result.status],
|
|
23280
23849
|
["Platform", platform],
|
|
23281
23850
|
["Profile", profile.name],
|
|
23282
|
-
["Runtime version", runtimeVersion],
|
|
23851
|
+
...runtimeVersion === void 0 ? [] : [["Runtime version", runtimeVersion]],
|
|
23283
23852
|
["Artifact", build.artifactPath],
|
|
23284
23853
|
["SHA-256", build.sha256],
|
|
23285
23854
|
["Bytes", String(build.byteSize)]
|
|
@@ -23324,7 +23893,7 @@ const DEFAULT_PROFILES = [
|
|
|
23324
23893
|
"preview",
|
|
23325
23894
|
"production"
|
|
23326
23895
|
];
|
|
23327
|
-
const writeEasJson
|
|
23896
|
+
const writeEasJson = (filePath, value) => Effect.gen(function* () {
|
|
23328
23897
|
yield* (yield* FileSystem.FileSystem).writeFileString(filePath, `${JSON.stringify(value, null, 2)}\n`).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to write eas.json: ${cause.message}` })));
|
|
23329
23898
|
});
|
|
23330
23899
|
const configureBuildCommand = defineCommand({
|
|
@@ -23342,7 +23911,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23342
23911
|
const easJsonPath = path.join(projectRoot, "eas.json");
|
|
23343
23912
|
const fs = yield* FileSystem.FileSystem;
|
|
23344
23913
|
if (!(yield* fs.exists(easJsonPath))) {
|
|
23345
|
-
yield* writeEasJson
|
|
23914
|
+
yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
|
|
23346
23915
|
yield* printHuman(`Wrote eas.json with default profiles to ${easJsonPath}.`);
|
|
23347
23916
|
yield* printHumanKeyValue([["Profiles", DEFAULT_PROFILES.join(", ")], ["Path", easJsonPath]]);
|
|
23348
23917
|
return {
|
|
@@ -23359,7 +23928,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23359
23928
|
path: easJsonPath
|
|
23360
23929
|
};
|
|
23361
23930
|
}
|
|
23362
|
-
yield* writeEasJson
|
|
23931
|
+
yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
|
|
23363
23932
|
yield* printHuman(`Overwrote eas.json with default profiles.`);
|
|
23364
23933
|
return {
|
|
23365
23934
|
action: "overwritten",
|
|
@@ -23387,7 +23956,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23387
23956
|
};
|
|
23388
23957
|
}
|
|
23389
23958
|
const additions = Object.fromEntries(missing.map((name) => [name, DEFAULT_EAS_JSON.build[name]]));
|
|
23390
|
-
yield* writeEasJson
|
|
23959
|
+
yield* writeEasJson(easJsonPath, {
|
|
23391
23960
|
build: {
|
|
23392
23961
|
...config.build,
|
|
23393
23962
|
...additions
|
|
@@ -23987,7 +24556,7 @@ const tryReadApkPackageWith = (bin, apkPath) => Effect.gen(function* () {
|
|
|
23987
24556
|
if (!(yield* which(bin).pipe(Effect.catchAll(() => Effect.succeed(null))))) return;
|
|
23988
24557
|
const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
23989
24558
|
if (!raw) return;
|
|
23990
|
-
return /package: name='([^']+)'/u.exec(raw)?.[1];
|
|
24559
|
+
return /package: name='(?<packageName>[^']+)'/u.exec(raw)?.[1];
|
|
23991
24560
|
});
|
|
23992
24561
|
const readApkPackageName = (apkPath) => Effect.gen(function* () {
|
|
23993
24562
|
return yield* Effect.reduce(["aapt2", "aapt"], void 0, (acc, bin) => acc === void 0 ? tryReadApkPackageWith(bin, apkPath) : Effect.succeed(acc));
|
|
@@ -24200,7 +24769,7 @@ const runCommand$1 = defineCommand({
|
|
|
24200
24769
|
//#region src/application/upload-workflow.ts
|
|
24201
24770
|
const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
|
|
24202
24771
|
if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
|
|
24203
|
-
if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier
|
|
24772
|
+
if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing iOS bundle identifier (set ios.bundleIdentifier or your Expo config)." });
|
|
24204
24773
|
return {
|
|
24205
24774
|
target: {
|
|
24206
24775
|
platform: "ios",
|
|
@@ -24212,7 +24781,7 @@ const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
|
|
|
24212
24781
|
});
|
|
24213
24782
|
const resolveAndroidTarget = (profile, appMeta, projectRoot) => Effect.gen(function* () {
|
|
24214
24783
|
if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
|
|
24215
|
-
if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing android.
|
|
24784
|
+
if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing Android applicationId (set android.applicationId or your Expo config)." });
|
|
24216
24785
|
const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
|
|
24217
24786
|
yield* warnOnGradleMismatch(gradleConfig, appMeta.androidPackage);
|
|
24218
24787
|
const bundleId = gradleConfig?.applicationId ?? appMeta.androidPackage;
|
|
@@ -24229,27 +24798,56 @@ const resolveAndroidTarget = (profile, appMeta, projectRoot) => Effect.gen(funct
|
|
|
24229
24798
|
bundleId
|
|
24230
24799
|
};
|
|
24231
24800
|
});
|
|
24801
|
+
/** Resolve app metadata + OTA runtimeVersion for an upload (project-type aware). */
|
|
24802
|
+
const resolveUploadMeta = (params) => Effect.gen(function* () {
|
|
24803
|
+
const { projectType, platform, projectRoot, profile, envVars } = params;
|
|
24804
|
+
const expoConfig = projectType === "expo" ? yield* readExpoConfig(projectRoot, envVars) : void 0;
|
|
24805
|
+
const appMeta = yield* resolveAppMeta({
|
|
24806
|
+
projectType,
|
|
24807
|
+
platform,
|
|
24808
|
+
projectRoot,
|
|
24809
|
+
profile,
|
|
24810
|
+
...compact({
|
|
24811
|
+
expoConfig,
|
|
24812
|
+
expoAppMeta: expoConfig === void 0 ? void 0 : yield* readAppMeta(expoConfig, platform)
|
|
24813
|
+
})
|
|
24814
|
+
});
|
|
24815
|
+
return {
|
|
24816
|
+
appMeta,
|
|
24817
|
+
runtimeVersion: expoConfig === void 0 ? void 0 : yield* resolveRuntimeVersion({
|
|
24818
|
+
raw: appMeta.rawRuntimeVersion,
|
|
24819
|
+
appVersion: appMeta.appVersion,
|
|
24820
|
+
projectRoot,
|
|
24821
|
+
platform,
|
|
24822
|
+
buildNumber: appMeta.buildNumber,
|
|
24823
|
+
sdkVersion: expoConfig.sdkVersion
|
|
24824
|
+
}),
|
|
24825
|
+
isExpo: expoConfig !== void 0
|
|
24826
|
+
};
|
|
24827
|
+
});
|
|
24232
24828
|
const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
24233
24829
|
const api = yield* apiClient;
|
|
24234
24830
|
const projectRoot = yield* (yield* CliRuntime).cwd;
|
|
24235
24831
|
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
24236
|
-
const
|
|
24832
|
+
const projectType = yield* detectProjectType({
|
|
24833
|
+
projectRoot,
|
|
24834
|
+
override: asProjectType((yield* readBetterUpdateConfig(projectRoot))?.["projectType"])
|
|
24835
|
+
});
|
|
24836
|
+
const projectId = yield* readProjectId;
|
|
24237
24837
|
const profile = yield* readBuildProfile(projectRoot, options.profileName);
|
|
24238
|
-
const
|
|
24838
|
+
const envVars = {
|
|
24239
24839
|
...yield* pullEnvVars(api, {
|
|
24240
24840
|
projectId,
|
|
24241
24841
|
environment: profile.environment
|
|
24242
24842
|
}),
|
|
24243
24843
|
...profile.env
|
|
24244
|
-
}
|
|
24245
|
-
const appMeta = yield*
|
|
24246
|
-
|
|
24247
|
-
raw: appMeta.rawRuntimeVersion,
|
|
24248
|
-
appVersion: appMeta.appVersion,
|
|
24249
|
-
projectRoot,
|
|
24844
|
+
};
|
|
24845
|
+
const { appMeta, runtimeVersion, isExpo } = yield* resolveUploadMeta({
|
|
24846
|
+
projectType,
|
|
24250
24847
|
platform: options.platform,
|
|
24251
|
-
|
|
24252
|
-
|
|
24848
|
+
projectRoot,
|
|
24849
|
+
profile,
|
|
24850
|
+
envVars
|
|
24253
24851
|
});
|
|
24254
24852
|
const { target, bundleId } = options.platform === "ios" ? yield* resolveIosTarget(profile, appMeta) : yield* resolveAndroidTarget(profile, appMeta, projectRoot);
|
|
24255
24853
|
yield* printHuman(`Hashing ${options.artifactPath}...`);
|
|
@@ -24260,7 +24858,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24260
24858
|
commit: rawGitContext.commit,
|
|
24261
24859
|
dirty: rawGitContext.dirty
|
|
24262
24860
|
});
|
|
24263
|
-
const fingerprintHash = yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
24861
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
|
|
24264
24862
|
const result = yield* reserveAndUpload(api, compact({
|
|
24265
24863
|
target,
|
|
24266
24864
|
projectId,
|
|
@@ -24282,7 +24880,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24282
24880
|
["Status", result.status],
|
|
24283
24881
|
["Platform", options.platform],
|
|
24284
24882
|
["Profile", profile.name],
|
|
24285
|
-
["Runtime version", runtimeVersion],
|
|
24883
|
+
...runtimeVersion === void 0 ? [] : [["Runtime version", runtimeVersion]],
|
|
24286
24884
|
["Artifact", options.artifactPath],
|
|
24287
24885
|
["SHA-256", sha256],
|
|
24288
24886
|
["Bytes", String(byteSize)]
|
|
@@ -24891,7 +25489,7 @@ const parseGoogleServiceAccountKey = (jsonText) => Effect.gen(function* () {
|
|
|
24891
25489
|
const APPLE_TEAM_ID_RE = /^[A-Z0-9]{10}$/u;
|
|
24892
25490
|
const extractTeamId = (params) => {
|
|
24893
25491
|
if (params.orgUnit && APPLE_TEAM_ID_RE.test(params.orgUnit)) return params.orgUnit;
|
|
24894
|
-
return /\(([A-Z0-9]{10})\)\s*$/u.exec(params.signingIdentity)?.[1];
|
|
25492
|
+
return /\((?<team>[A-Z0-9]{10})\)\s*$/u.exec(params.signingIdentity)?.[1];
|
|
24895
25493
|
};
|
|
24896
25494
|
/**
|
|
24897
25495
|
* Parse a PKCS#12 (.p12) buffer and extract certificate metadata.
|
|
@@ -28776,7 +29374,7 @@ const DEVICE_CLASS_VALUES = [
|
|
|
28776
29374
|
const isDeviceClass = (value) => DEVICE_CLASS_VALUES.includes(value);
|
|
28777
29375
|
const ttlHours = (value) => {
|
|
28778
29376
|
if (value === void 0) return;
|
|
28779
|
-
const match = /^([0-9]+)([hd])?$/u.exec(value);
|
|
29377
|
+
const match = /^(?<value>[0-9]+)(?<unit>[hd])?$/u.exec(value);
|
|
28780
29378
|
if (!match?.[1]) return;
|
|
28781
29379
|
const num = Number.parseInt(match[1], 10);
|
|
28782
29380
|
return match[2] === "d" ? num * 24 : num;
|
|
@@ -29186,11 +29784,18 @@ const checkProjectLink = Effect.gen(function* () {
|
|
|
29186
29784
|
const source = (yield* readLinkedProjectId(root)) === void 0 ? "Expo config" : "better-update.json";
|
|
29187
29785
|
return pass("project-linked", "Project linked", `projectId=${resolved.right} (via ${source})`);
|
|
29188
29786
|
});
|
|
29189
|
-
const
|
|
29190
|
-
const
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
29787
|
+
const checkProjectType = Effect.gen(function* () {
|
|
29788
|
+
const root = yield* (yield* CliRuntime).cwd;
|
|
29789
|
+
const override = asProjectType((yield* readBetterUpdateConfig(root))?.["projectType"]);
|
|
29790
|
+
return pass("project-type", "Project type", `${yield* detectProjectType({
|
|
29791
|
+
projectRoot: root,
|
|
29792
|
+
override
|
|
29793
|
+
})} (${override === void 0 ? "auto-detected" : "better-update.json override"})`);
|
|
29794
|
+
});
|
|
29795
|
+
const checkBuildConfig = Effect.gen(function* () {
|
|
29796
|
+
const names = yield* listBuildProfileNames(yield* (yield* CliRuntime).cwd);
|
|
29797
|
+
if (names.length === 0) return warn("build-config", "Build config", "No build profiles found. Add a \"build\" section to better-update.json.");
|
|
29798
|
+
return pass("build-config", "Build config", `${names.length} profile(s) defined`);
|
|
29194
29799
|
});
|
|
29195
29800
|
const runChecks = Effect.gen(function* () {
|
|
29196
29801
|
const xcode = (yield* CliRuntime).platform === "darwin" ? [yield* checkCommand("xcode", "Xcode CLI tools", "xcode-select", ["-p"])] : [];
|
|
@@ -29201,7 +29806,8 @@ const runChecks = Effect.gen(function* () {
|
|
|
29201
29806
|
yield* checkServerHealth,
|
|
29202
29807
|
yield* checkAuth,
|
|
29203
29808
|
yield* checkProjectLink,
|
|
29204
|
-
yield*
|
|
29809
|
+
yield* checkProjectType,
|
|
29810
|
+
yield* checkBuildConfig
|
|
29205
29811
|
];
|
|
29206
29812
|
});
|
|
29207
29813
|
const statusIcon = (status) => {
|
|
@@ -29260,7 +29866,7 @@ const parseSingleEnvironmentArg = (raw) => Effect.gen(function* () {
|
|
|
29260
29866
|
return raw;
|
|
29261
29867
|
});
|
|
29262
29868
|
const formatEnvironments = (environments) => [...environments].toSorted((left, right) => left.localeCompare(right)).join(",");
|
|
29263
|
-
const DOTENV_LINE = /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(
|
|
29869
|
+
const DOTENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Z][A-Z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
|
|
29264
29870
|
const stripQuotes = (raw) => {
|
|
29265
29871
|
if (raw.length < 2) return raw;
|
|
29266
29872
|
const [first] = raw;
|
|
@@ -30369,49 +30975,40 @@ const logoutCommand = defineCommand({
|
|
|
30369
30975
|
|
|
30370
30976
|
//#endregion
|
|
30371
30977
|
//#region src/commands/migrate-config.ts
|
|
30372
|
-
const readAppJson = (projectRoot) => {
|
|
30373
|
-
const path$1 = path.join(projectRoot, "app.json");
|
|
30374
|
-
if (!existsSync(path$1)) return null;
|
|
30375
|
-
const raw = readFileSync(path$1, "utf8");
|
|
30376
|
-
return JSON.parse(raw);
|
|
30377
|
-
};
|
|
30378
|
-
const writeAppJson = (projectRoot, content) => {
|
|
30379
|
-
writeFileSync(path.join(projectRoot, "app.json"), `${JSON.stringify(content, null, 2)}\n`);
|
|
30380
|
-
};
|
|
30381
|
-
const writeEasJson = (projectRoot, profiles) => {
|
|
30382
|
-
writeFileSync(path.join(projectRoot, "eas.json"), `${JSON.stringify({ build: profiles }, null, 2)}\n`);
|
|
30383
|
-
};
|
|
30384
30978
|
const migrateConfigCommand = defineCommand({
|
|
30385
30979
|
meta: {
|
|
30386
30980
|
name: "migrate-config",
|
|
30387
|
-
description: "Migrate
|
|
30981
|
+
description: "Migrate `build`/`submit` profiles from a legacy eas.json into better-update.json"
|
|
30388
30982
|
},
|
|
30389
30983
|
args: { yes: {
|
|
30390
30984
|
type: "boolean",
|
|
30391
30985
|
description: "Skip the confirmation prompt"
|
|
30392
30986
|
} },
|
|
30393
30987
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30394
|
-
const
|
|
30395
|
-
const
|
|
30396
|
-
|
|
30397
|
-
const
|
|
30398
|
-
if (
|
|
30399
|
-
|
|
30988
|
+
const runtime = yield* CliRuntime;
|
|
30989
|
+
const fs = yield* FileSystem.FileSystem;
|
|
30990
|
+
const root = yield* runtime.cwd;
|
|
30991
|
+
const easPath = path.join(root, "eas.json");
|
|
30992
|
+
if (!(yield* fs.exists(easPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new InvalidArgumentError({ message: `No eas.json found at ${root}.` });
|
|
30993
|
+
const config = yield* readEasJson(root);
|
|
30994
|
+
const patch = compact({
|
|
30995
|
+
build: config.build,
|
|
30996
|
+
submit: config.submit,
|
|
30997
|
+
cli: config.cli
|
|
30998
|
+
});
|
|
30999
|
+
if (Object.keys(patch).length === 0) {
|
|
31000
|
+
yield* printHuman("eas.json has no build/submit/cli sections — nothing to migrate.");
|
|
30400
31001
|
return;
|
|
30401
31002
|
}
|
|
30402
|
-
|
|
30403
|
-
if (!args.yes) {
|
|
30404
|
-
if (!(yield* promptConfirm(
|
|
31003
|
+
const existingBuild = (yield* readBetterUpdateConfig(root))?.["build"];
|
|
31004
|
+
if (typeof existingBuild === "object" && existingBuild !== null && config.build !== void 0 && !args.yes) {
|
|
31005
|
+
if (!(yield* promptConfirm("better-update.json already has a build section — overwrite it from eas.json?", { initialValue: false }))) {
|
|
30405
31006
|
yield* printHuman("Cancelled.");
|
|
30406
31007
|
return;
|
|
30407
31008
|
}
|
|
30408
31009
|
}
|
|
30409
|
-
|
|
30410
|
-
|
|
30411
|
-
const betterUpdate = (clone.expo?.extra)?.betterUpdate;
|
|
30412
|
-
if (betterUpdate) delete betterUpdate["profiles"];
|
|
30413
|
-
writeAppJson(root, clone);
|
|
30414
|
-
yield* printHuman("Migrated profiles into eas.json. Legacy field removed from app.json.");
|
|
31010
|
+
yield* writeBetterUpdateConfig(root, patch);
|
|
31011
|
+
yield* printHuman("Merged eas.json build/submit into better-update.json. You can now delete eas.json.");
|
|
30415
31012
|
}))
|
|
30416
31013
|
});
|
|
30417
31014
|
|
|
@@ -30856,7 +31453,7 @@ const submitCommand = defineCommand({
|
|
|
30856
31453
|
}
|
|
30857
31454
|
const projectId = yield* readProjectId;
|
|
30858
31455
|
const api = yield* apiClient;
|
|
30859
|
-
const easProfile = yield*
|
|
31456
|
+
const easProfile = yield* readSubmitProfile(yield* (yield* CliRuntime).cwd, args.profile);
|
|
30860
31457
|
const archive = yield* resolveArchive(api, projectId, platform, {
|
|
30861
31458
|
id: args.id,
|
|
30862
31459
|
path: args.path,
|