pakstr 0.8.7 → 0.9.0

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.
@@ -7,11 +7,9 @@ exports.DockerGradleRunner = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const crypto_1 = require("crypto");
10
- const releaseVerification_1 = require("../android/releaseVerification");
11
- const releaseSigning_1 = require("../core/releaseSigning");
12
10
  const process_1 = require("./process");
13
- const CONTAINER_KEYSTORE_PATH = "/run/secrets/pakstr/release.keystore";
14
- const CONTAINER_RELEASE_APK = "/workspace/app/build/outputs/apk/release/app-release.apk";
11
+ const CONTAINER_RELEASE_APK_SIGNED = "/workspace/app/build/outputs/apk/release/app-release.apk";
12
+ const CONTAINER_RELEASE_APK_UNSIGNED = "/workspace/app/build/outputs/apk/release/app-release-unsigned.apk";
15
13
  const CONTAINER_DEBUG_APK = "/workspace/app/build/outputs/apk/debug/app-debug.apk";
16
14
  class DockerGradleRunner {
17
15
  image;
@@ -24,14 +22,13 @@ class DockerGradleRunner {
24
22
  async build(request) {
25
23
  this.checkDockerAvailable();
26
24
  this.ensureDockerImage();
27
- const signing = request.mode === "release" ? requireSigning(request.signing) : undefined;
28
- const storage = this.createBuildStorage(request.androidRoot, signing);
25
+ const storage = this.createBuildStorage(request.androidRoot);
29
26
  let primaryError;
30
27
  let result;
31
28
  try {
32
29
  result =
33
30
  request.mode === "release"
34
- ? this.buildRelease(request, signing, storage)
31
+ ? this.buildRelease(request, storage)
35
32
  : this.buildDebug(request, storage);
36
33
  }
37
34
  catch (error) {
@@ -46,7 +43,7 @@ class DockerGradleRunner {
46
43
  }
47
44
  if (primaryError) {
48
45
  if (cleanupError) {
49
- console.error("Docker signing storage cleanup also failed:", safeErrorMessage(cleanupError));
46
+ console.error("Docker build storage cleanup also failed:", safeErrorMessage(cleanupError));
50
47
  }
51
48
  throw primaryError;
52
49
  }
@@ -66,74 +63,38 @@ class DockerGradleRunner {
66
63
  "./gradlew",
67
64
  "assembleDebug",
68
65
  "-Dorg.gradle.vfs.watch=false",
69
- ], {
70
- env: (0, releaseSigning_1.removeSigningVariables)(process.env),
71
- streamOutput: true,
72
- });
66
+ ], { streamOutput: true });
73
67
  const artifactPath = hostArtifactPath(request.androidRoot, "debug");
74
68
  this.copyFromVolume(storage.workspaceVolume, CONTAINER_DEBUG_APK, artifactPath);
75
- return { artifactPath: requireApk(artifactPath) };
69
+ return { artifactPath: requireApk(artifactPath), unsigned: false };
76
70
  }
77
- buildRelease(request, signing, storage) {
78
- if (!storage.signingVolume) {
79
- throw new Error("Docker release build requires signing storage");
80
- }
81
- const secrets = signingSecrets(signing);
82
- const dockerEnv = (0, releaseSigning_1.createGradleSigningEnvironment)(signing, process.env, CONTAINER_KEYSTORE_PATH);
83
- const environmentNames = [
84
- releaseSigning_1.INTERNAL_KEYSTORE_PATH_ENV,
85
- releaseSigning_1.PUBLIC_SIGNING_ENV.keystorePassword,
86
- releaseSigning_1.PUBLIC_SIGNING_ENV.keyAlias,
87
- releaseSigning_1.PUBLIC_SIGNING_ENV.keyPassword,
88
- ];
71
+ buildRelease(request, storage) {
72
+ // Unsigned release build: no signing config is wired up because
73
+ // PAKSTR_ANDROID_KEYSTORE_PATH is not set in the container environment.
89
74
  this.execute("docker", [
90
75
  "run",
91
76
  "--rm",
92
77
  "-v",
93
78
  `${storage.workspaceVolume}:/workspace`,
94
- "-v",
95
- `${storage.signingVolume}:/run/secrets/pakstr:ro`,
96
- ...environmentNames.flatMap(name => ["--env", name]),
97
79
  this.image,
98
80
  "./gradlew",
99
81
  "assembleRelease",
100
82
  "--no-daemon",
101
83
  "-Dorg.gradle.vfs.watch=false",
102
- ], {
103
- env: dockerEnv,
104
- secrets,
105
- streamOutput: true,
106
- });
107
- const verification = (0, releaseVerification_1.verifyReleaseApk)({
108
- artifactPath: CONTAINER_RELEASE_APK,
109
- expectedMetadata: request.expectedMetadata,
110
- signing,
111
- toolKeystorePath: CONTAINER_KEYSTORE_PATH,
112
- executor: this.createVerificationExecutor(storage, signing),
113
- });
114
- const artifactPath = hostArtifactPath(request.androidRoot, "release");
115
- this.copyFromVolume(storage.workspaceVolume, CONTAINER_RELEASE_APK, artifactPath);
116
- return { artifactPath: requireApk(artifactPath), verification };
84
+ ], { env: scrubSigningEnv(process.env), streamOutput: true });
85
+ const artifactPath = hostArtifactPath(request.androidRoot, "release-unsigned");
86
+ this.copyFromVolume(storage.workspaceVolume, CONTAINER_RELEASE_APK_UNSIGNED, artifactPath);
87
+ return { artifactPath: requireApk(artifactPath), unsigned: true };
117
88
  }
118
- createBuildStorage(androidRoot, signing) {
89
+ createBuildStorage(androidRoot) {
119
90
  const suffix = (0, crypto_1.randomUUID)();
120
91
  const storage = {
121
92
  workspaceVolume: `pakstr-workspace-${suffix}`,
122
- signingVolume: signing ? `pakstr-signing-${suffix}` : undefined,
123
93
  };
124
94
  assertRegularAbsolutePath(androidRoot, "Android workspace");
125
- if (signing) {
126
- assertRegularAbsolutePath(signing.keystorePath, "Temporary keystore", true);
127
- }
128
95
  try {
129
96
  this.execute("docker", ["volume", "create", storage.workspaceVolume]);
130
- if (storage.signingVolume) {
131
- this.execute("docker", ["volume", "create", storage.signingVolume]);
132
- }
133
97
  this.copyDirectoryToVolume(androidRoot, storage.workspaceVolume);
134
- if (signing && storage.signingVolume) {
135
- this.copyFileToVolume(signing.keystorePath, storage.signingVolume, "/run/secrets/pakstr/release.keystore", signingSecrets(signing));
136
- }
137
98
  return storage;
138
99
  }
139
100
  catch (error) {
@@ -141,7 +102,7 @@ class DockerGradleRunner {
141
102
  this.removeBuildStorage(storage);
142
103
  }
143
104
  catch (cleanupError) {
144
- console.error("Docker signing storage cleanup also failed:", safeErrorMessage(cleanupError));
105
+ console.error("Docker build storage cleanup also failed:", safeErrorMessage(cleanupError));
145
106
  }
146
107
  throw error;
147
108
  }
@@ -164,26 +125,6 @@ class DockerGradleRunner {
164
125
  this.removeContainer(container);
165
126
  }
166
127
  }
167
- copyFileToVolume(source, volume, destination, secrets) {
168
- const container = `pakstr-signing-copy-${(0, crypto_1.randomUUID)()}`;
169
- try {
170
- this.execute("docker", [
171
- "create",
172
- "--name",
173
- container,
174
- "-v",
175
- `${volume}:/run/secrets/pakstr`,
176
- this.image,
177
- "true",
178
- ]);
179
- this.execute("docker", ["cp", "-a", source, `${container}:${destination}`], {
180
- secrets,
181
- });
182
- }
183
- finally {
184
- this.removeContainer(container);
185
- }
186
- }
187
128
  copyFromVolume(volume, source, destination) {
188
129
  const container = `pakstr-output-copy-${(0, crypto_1.randomUUID)()}`;
189
130
  fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
@@ -204,43 +145,12 @@ class DockerGradleRunner {
204
145
  this.removeContainer(container);
205
146
  }
206
147
  }
207
- createVerificationExecutor(storage, signing) {
208
- const secrets = signingSecrets(signing);
209
- return {
210
- run: (tool, args, env = {}) => {
211
- const volumeArgs = ["-v", `${storage.workspaceVolume}:/workspace:ro`];
212
- if (tool === "keytool") {
213
- if (!storage.signingVolume) {
214
- throw new Error("Signer verification requires Docker signing storage");
215
- }
216
- volumeArgs.push("-v", `${storage.signingVolume}:/run/secrets/pakstr:ro`);
217
- }
218
- return this.execute("docker", [
219
- "run",
220
- "--rm",
221
- ...volumeArgs,
222
- ...Object.keys(env).flatMap(name => ["--env", name]),
223
- this.image,
224
- tool,
225
- ...args,
226
- ], { env: { ...(0, releaseSigning_1.removeSigningVariables)(process.env), ...env }, secrets });
227
- },
228
- };
229
- }
230
148
  removeBuildStorage(storage) {
231
- const failures = [];
232
- for (const volume of [storage.signingVolume, storage.workspaceVolume]) {
233
- if (!volume)
234
- continue;
235
- try {
236
- this.execute("docker", ["volume", "rm", "--force", volume]);
237
- }
238
- catch {
239
- failures.push(volume);
240
- }
149
+ try {
150
+ this.execute("docker", ["volume", "rm", "--force", storage.workspaceVolume]);
241
151
  }
242
- if (failures.length) {
243
- throw new Error(`Failed to remove temporary Docker volume(s): ${failures.join(", ")}`);
152
+ catch {
153
+ throw new Error(`Failed to remove temporary Docker volume: ${storage.workspaceVolume}`);
244
154
  }
245
155
  }
246
156
  removeContainer(container) {
@@ -256,7 +166,7 @@ class DockerGradleRunner {
256
166
  this.execute("docker", ["--version"]);
257
167
  }
258
168
  catch {
259
- throw new Error("Docker is not installed or not running. Please install Docker Desktop first.");
169
+ throw new Error("Docker is not installed or not running. Install Docker Desktop to use pakstr.");
260
170
  }
261
171
  }
262
172
  ensureDockerImage() {
@@ -271,8 +181,11 @@ class DockerGradleRunner {
271
181
  }
272
182
  }
273
183
  exports.DockerGradleRunner = DockerGradleRunner;
274
- function hostArtifactPath(androidRoot, mode) {
275
- return path_1.default.join(androidRoot, "app", "build", "outputs", "apk", mode, `app-${mode}.apk`);
184
+ function hostArtifactPath(androidRoot, variant) {
185
+ const file = variant === "debug"
186
+ ? "app-debug.apk"
187
+ : "app-release-unsigned.apk";
188
+ return path_1.default.join(androidRoot, "app", "build", "outputs", "apk", variant === "debug" ? "debug" : "release", file);
276
189
  }
277
190
  function requireApk(artifactPath) {
278
191
  if (!fs_1.default.existsSync(artifactPath) || !fs_1.default.lstatSync(artifactPath).isFile()) {
@@ -280,21 +193,18 @@ function requireApk(artifactPath) {
280
193
  }
281
194
  return artifactPath;
282
195
  }
283
- function requireSigning(signing) {
284
- if (!signing) {
285
- throw new Error("Release build requires an internal signing context");
286
- }
287
- return signing;
288
- }
289
- function signingSecrets(signing) {
290
- return [
291
- signing.storePassword,
292
- signing.keyAlias,
293
- signing.keyPassword,
294
- signing.keystorePath,
295
- ];
196
+ function scrubSigningEnv(env) {
197
+ const sanitized = { ...env };
198
+ delete sanitized["PAKSTR_ANDROID_KEYSTORE_PATH"];
199
+ delete sanitized["PAKSTR_ANDROID_KEYSTORE_PASSWORD"];
200
+ delete sanitized["PAKSTR_ANDROID_KEY_ALIAS"];
201
+ delete sanitized["PAKSTR_ANDROID_KEY_PASSWORD"];
202
+ delete sanitized["PAKSTR_ANDROID_SIGNING_MODE"];
203
+ delete sanitized["APP_NSEC"];
204
+ delete sanitized["KEYSTORE_PASSWORD"];
205
+ return sanitized;
296
206
  }
297
- function assertRegularAbsolutePath(candidate, label, requireFile = false) {
207
+ function assertRegularAbsolutePath(candidate, label) {
298
208
  if (!path_1.default.isAbsolute(candidate)) {
299
209
  throw new Error(`${label} path must be absolute`);
300
210
  }
@@ -305,7 +215,7 @@ function assertRegularAbsolutePath(candidate, label, requireFile = false) {
305
215
  if (stat.isSymbolicLink()) {
306
216
  throw new Error(`${label} path must not be a symbolic link`);
307
217
  }
308
- if (requireFile ? !stat.isFile() : !stat.isDirectory()) {
218
+ if (!stat.isDirectory()) {
309
219
  throw new Error(`${label} path has an unexpected filesystem type`);
310
220
  }
311
221
  }
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DockerSignRunner = void 0;
7
+ const crypto_1 = require("crypto");
8
+ const crypto_2 = require("crypto");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const process_1 = require("./process");
13
+ class DockerSignRunner {
14
+ image;
15
+ execute;
16
+ constructor(image = process.env.PAKSTR_DOCKER_IMAGE ??
17
+ "git.nostrdev.com/stuff/pakstr/android-builder:latest", execute = process_1.runProcess) {
18
+ this.image = image;
19
+ this.execute = execute;
20
+ }
21
+ async sign(request) {
22
+ this.checkDockerAvailable();
23
+ assertRegularFile(request.unsignedApkPath, "Unsigned APK");
24
+ // Use a Docker named volume for the unsigned APK (not a bind mount). The
25
+ // Gitea act runner runs jobs inside a container, so single-file bind
26
+ // mounts from the host path don't reliably reach the inner docker daemon;
27
+ // a named volume copied via a temp container works across that boundary
28
+ // (mirroring how DockerGradleRunner transports the workspace).
29
+ const apkVolume = `pakstr-sign-apk-${(0, crypto_2.randomUUID)()}`;
30
+ const outVolume = `pakstr-sign-out-${(0, crypto_2.randomUUID)()}`;
31
+ let primaryError;
32
+ let result;
33
+ try {
34
+ this.execute("docker", ["volume", "create", apkVolume]);
35
+ this.execute("docker", ["volume", "create", outVolume]);
36
+ this.copyFileToVolume(request.unsignedApkPath, apkVolume, "/work/unsigned.apk");
37
+ const password = (0, crypto_1.randomBytes)(32).toString("base64url");
38
+ const nsecString = nsecToString(request.nsec);
39
+ const env = {
40
+ ...process.env,
41
+ APP_NSEC: nsecString,
42
+ KEYSTORE_PASSWORD: password,
43
+ };
44
+ // Explicitly forward the two signing env vars into the container; docker
45
+ // run does not auto-forward the parent environment.
46
+ const signingEnvNames = ["APP_NSEC", "KEYSTORE_PASSWORD"];
47
+ const stdout = this.execute("docker", [
48
+ "run",
49
+ "--rm",
50
+ "--tmpfs",
51
+ "/signing:mode=0700,uid=0",
52
+ "-v",
53
+ `${apkVolume}:/work:ro`,
54
+ "-v",
55
+ `${outVolume}:/out`,
56
+ ...signingEnvNames.flatMap(name => ["--env", name]),
57
+ this.image,
58
+ "/opt/pakstr/sign.sh",
59
+ request.appId,
60
+ "/work/unsigned.apk",
61
+ "/out/signed.apk",
62
+ ], { env, secrets: [nsecString, password], streamOutput: true }).stdout;
63
+ const signerSha256 = parseSignerSha(stdout);
64
+ // Copy the signed APK out of the output volume to the host.
65
+ const hostSigned = path_1.default.join(os_1.default.tmpdir(), `pakstr-signed-${(0, crypto_2.randomUUID)()}.apk`);
66
+ this.copyFromVolume(outVolume, "/out/signed.apk", hostSigned);
67
+ if (!fs_1.default.existsSync(hostSigned) || !fs_1.default.lstatSync(hostSigned).isFile()) {
68
+ throw new Error("Sign runner did not produce a signed APK");
69
+ }
70
+ fs_1.default.mkdirSync(path_1.default.dirname(request.signedApkPath), { recursive: true });
71
+ fs_1.default.rmSync(request.signedApkPath, { force: true });
72
+ const finalPath = path_1.default.resolve(request.signedApkPath);
73
+ // Copy (not rename) across the tmpfs → workspace device boundary; CI
74
+ // runners put /tmp and the checkout on different filesystems, so
75
+ // fs.rename fails with EXDEV. COPYFILE_FICLONE is a harmless hint.
76
+ fs_1.default.copyFileSync(hostSigned, finalPath, fs_1.default.constants.COPYFILE_FICLONE);
77
+ fs_1.default.rmSync(hostSigned, { force: true });
78
+ result = { signedApkPath: finalPath, signerSha256 };
79
+ }
80
+ catch (error) {
81
+ primaryError = error;
82
+ }
83
+ finally {
84
+ for (const v of [outVolume, apkVolume]) {
85
+ try {
86
+ this.execute("docker", ["volume", "rm", "--force", v]);
87
+ }
88
+ catch {
89
+ /* best-effort cleanup */
90
+ }
91
+ }
92
+ }
93
+ if (primaryError)
94
+ throw primaryError;
95
+ if (!result)
96
+ throw new Error("Sign runner completed without a result");
97
+ return result;
98
+ }
99
+ copyFileToVolume(source, volume, destination) {
100
+ const container = `pakstr-sign-copy-${(0, crypto_2.randomUUID)()}`;
101
+ try {
102
+ this.execute("docker", [
103
+ "create",
104
+ "--name",
105
+ container,
106
+ "-v",
107
+ `${volume}:/work`,
108
+ this.image,
109
+ "true",
110
+ ]);
111
+ this.execute("docker", ["cp", source, `${container}:${destination}`]);
112
+ }
113
+ finally {
114
+ this.removeContainer(container);
115
+ }
116
+ }
117
+ copyFromVolume(volume, source, destination) {
118
+ // Mount the volume at the same directory the source path lives under so
119
+ // the in-container path matches what the producing container wrote.
120
+ const mountDir = path_1.default.posix.dirname(source);
121
+ const container = `pakstr-sign-out-${(0, crypto_2.randomUUID)()}`;
122
+ fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
123
+ fs_1.default.rmSync(destination, { force: true });
124
+ try {
125
+ this.execute("docker", [
126
+ "create",
127
+ "--name",
128
+ container,
129
+ "-v",
130
+ `${volume}:${mountDir}:ro`,
131
+ this.image,
132
+ "true",
133
+ ]);
134
+ this.execute("docker", ["cp", `${container}:${source}`, destination]);
135
+ }
136
+ finally {
137
+ this.removeContainer(container);
138
+ }
139
+ }
140
+ removeContainer(container) {
141
+ try {
142
+ this.execute("docker", ["rm", "--force", container]);
143
+ }
144
+ catch {
145
+ /* a failed create leaves no container */
146
+ }
147
+ }
148
+ checkDockerAvailable() {
149
+ try {
150
+ this.execute("docker", ["--version"]);
151
+ }
152
+ catch {
153
+ throw new Error("Docker is not installed or not running. Install Docker Desktop to use pakstr.");
154
+ }
155
+ }
156
+ }
157
+ exports.DockerSignRunner = DockerSignRunner;
158
+ function parseSignerSha(stdout) {
159
+ const match = stdout.match(/PAKSTR_SIGN_OK sha256=([0-9a-fA-F]+)/);
160
+ if (!match || !match[1]) {
161
+ throw new Error("Sign runner did not confirm the signer certificate");
162
+ }
163
+ return match[1].toLowerCase();
164
+ }
165
+ /** Re-encode the secret bytes as a bech32 nsec string for the container env. */
166
+ function nsecToString(secret) {
167
+ // Lazy import to avoid a circular type dependency at module load.
168
+ const { encodeNsec } = require("../core/nostr");
169
+ return encodeNsec(secret);
170
+ }
171
+ function assertRegularFile(candidate, label) {
172
+ if (!path_1.default.isAbsolute(candidate)) {
173
+ throw new Error(`${label} path must be absolute: ${candidate}`);
174
+ }
175
+ if (!fs_1.default.existsSync(candidate)) {
176
+ throw new Error(`${label} not found: ${candidate}`);
177
+ }
178
+ const stat = fs_1.default.lstatSync(candidate);
179
+ if (!stat.isFile() || stat.isSymbolicLink()) {
180
+ throw new Error(`${label} must be a regular file: ${candidate}`);
181
+ }
182
+ }
@@ -5,11 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.LocalGradleRunner = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
- const os_1 = __importDefault(require("os"));
9
8
  const path_1 = __importDefault(require("path"));
10
- const releaseVerification_1 = require("../android/releaseVerification");
11
- const releaseSigning_1 = require("../core/releaseSigning");
12
9
  const process_1 = require("./process");
10
+ /**
11
+ * Local Gradle runner for users who already have a JDK + Android SDK + Gradle
12
+ * installed. The default and only specified builder is `docker`; this remains
13
+ * as a convenience for local development.
14
+ */
13
15
  class LocalGradleRunner {
14
16
  async build(request) {
15
17
  const { androidRoot, mode } = request;
@@ -22,143 +24,38 @@ class LocalGradleRunner {
22
24
  if (mode === "debug") {
23
25
  (0, process_1.runProcess)(gradlewPath, ["assembleDebug"], {
24
26
  cwd: androidRoot,
25
- env: (0, releaseSigning_1.removeSigningVariables)(process.env),
26
27
  streamOutput: true,
27
28
  });
28
- return {
29
- artifactPath: requireApk(androidRoot, "debug"),
30
- };
29
+ return { artifactPath: requireApk(androidRoot, "debug"), unsigned: false };
31
30
  }
32
- const signing = requireSigning(request.signing);
33
- const secrets = signingSecrets(signing);
34
- const gradleEnv = (0, releaseSigning_1.createGradleSigningEnvironment)(signing);
35
- (0, process_1.runProcess)(gradlewPath, [
36
- "assembleRelease",
37
- "--no-daemon",
38
- ], {
31
+ // Unsigned release: ensure no signing env leaks into Gradle.
32
+ const env = scrubSigningEnv(process.env);
33
+ (0, process_1.runProcess)(gradlewPath, ["assembleRelease", "--no-daemon"], {
39
34
  cwd: androidRoot,
40
- env: gradleEnv,
41
- secrets,
35
+ env,
42
36
  streamOutput: true,
43
37
  });
44
- const artifactPath = requireApk(androidRoot, "release");
45
- let verification;
46
- try {
47
- const executor = createLocalVerificationExecutor(signing);
48
- verification =
49
- (0, releaseVerification_1.verifyReleaseApk)({
50
- artifactPath,
51
- expectedMetadata: request.expectedMetadata,
52
- signing,
53
- executor,
54
- });
55
- }
56
- catch (error) {
57
- if (isMissingLocalVerificationTool(error)) {
58
- console.warn("⚠️ Local release verification skipped: Android verification tools are unavailable.");
59
- console.warn("ℹ️ Full release verification will still run in Docker/CI.");
60
- verification = undefined;
61
- }
62
- else {
63
- throw error;
64
- }
65
- }
66
- return {
67
- artifactPath,
68
- verification,
69
- };
38
+ const artifactPath = requireApk(androidRoot, "release-unsigned");
39
+ return { artifactPath, unsigned: true };
70
40
  }
71
41
  }
72
42
  exports.LocalGradleRunner = LocalGradleRunner;
73
- function isMissingLocalVerificationTool(error) {
74
- if (!(error instanceof Error)) {
75
- return false;
76
- }
77
- const messages = [
78
- "verification tool",
79
- "cmdline-tools",
80
- "build-tools directory",
81
- "Required Android verification tool",
82
- ];
83
- return messages.some(message => error.message.includes(message));
84
- }
85
- function createLocalVerificationExecutor(signing) {
86
- const androidHome = process.env.ANDROID_HOME ??
87
- process.env.ANDROID_SDK_ROOT ??
88
- path_1.default.join(os_1.default.homedir(), "Library", "Android", "sdk");
89
- const secrets = signingSecrets(signing);
90
- return {
91
- run(tool, args, env = {}) {
92
- const command = resolveTool(tool, androidHome);
93
- return (0, process_1.runProcess)(command, args, {
94
- env: {
95
- ...process.env,
96
- ...env,
97
- },
98
- secrets,
99
- });
100
- },
101
- };
102
- }
103
- function resolveTool(tool, androidHome) {
104
- if (tool === "keytool") {
105
- return "keytool";
106
- }
107
- if (tool === "apkanalyzer") {
108
- const cmdlineTools = path_1.default.join(androidHome, "cmdline-tools");
109
- if (!fs_1.default.existsSync(cmdlineTools)) {
110
- throw new Error(`Android cmdline-tools directory not found: ${cmdlineTools}`);
111
- }
112
- const versions = fs_1.default.readdirSync(cmdlineTools)
113
- .sort((a, b) => b.localeCompare(a, undefined, {
114
- numeric: true,
115
- }));
116
- const analyzer = versions
117
- .map(version => path_1.default.join(cmdlineTools, version, "bin", "apkanalyzer"))
118
- .find(candidate => fs_1.default.existsSync(candidate));
119
- if (!analyzer) {
120
- throw new Error(`Required Android verification tool not found under ${cmdlineTools}`);
121
- }
122
- return analyzer;
123
- }
124
- const buildToolsDir = path_1.default.join(androidHome, "build-tools");
125
- if (!fs_1.default.existsSync(buildToolsDir)) {
126
- throw new Error(`Android build-tools directory not found: ${buildToolsDir}`);
127
- }
128
- const versions = fs_1.default.readdirSync(buildToolsDir)
129
- .sort((a, b) => b.localeCompare(a, undefined, {
130
- numeric: true,
131
- }));
132
- const executable = tool === "aapt"
133
- ? "aapt"
134
- : "apksigner";
135
- const resolved = versions
136
- .map(version => path_1.default.join(buildToolsDir, version, executable))
137
- .find(candidate => fs_1.default.existsSync(candidate));
138
- if (!resolved) {
139
- throw new Error(`Required Android verification tool ${executable} not found under ${buildToolsDir}`);
140
- }
141
- return resolved;
142
- }
143
- function requireApk(androidRoot, mode) {
144
- const artifactPath = path_1.default.join(androidRoot, "app", "build", "outputs", "apk", mode, `app-${mode}.apk`);
145
- if (!fs_1.default.existsSync(artifactPath) ||
146
- !fs_1.default.lstatSync(artifactPath).isFile()) {
43
+ function requireApk(androidRoot, variant) {
44
+ const file = variant === "debug" ? "app-debug.apk" : "app-release-unsigned.apk";
45
+ const artifactPath = path_1.default.join(androidRoot, "app", "build", "outputs", "apk", variant === "debug" ? "debug" : "release", file);
46
+ if (!fs_1.default.existsSync(artifactPath) || !fs_1.default.lstatSync(artifactPath).isFile()) {
147
47
  throw new Error(`APK not found after Gradle build: ${artifactPath}`);
148
48
  }
149
49
  return artifactPath;
150
50
  }
151
- function requireSigning(signing) {
152
- if (!signing) {
153
- throw new Error("Release build requires an internal signing context");
154
- }
155
- return signing;
156
- }
157
- function signingSecrets(signing) {
158
- return [
159
- signing.storePassword,
160
- signing.keyAlias,
161
- signing.keyPassword,
162
- signing.keystorePath,
163
- ];
51
+ function scrubSigningEnv(env) {
52
+ const sanitized = { ...env };
53
+ delete sanitized["PAKSTR_ANDROID_KEYSTORE_PATH"];
54
+ delete sanitized["PAKSTR_ANDROID_KEYSTORE_PASSWORD"];
55
+ delete sanitized["PAKSTR_ANDROID_KEY_ALIAS"];
56
+ delete sanitized["PAKSTR_ANDROID_KEY_PASSWORD"];
57
+ delete sanitized["PAKSTR_ANDROID_SIGNING_MODE"];
58
+ delete sanitized["APP_NSEC"];
59
+ delete sanitized["KEYSTORE_PASSWORD"];
60
+ return sanitized;
164
61
  }
@@ -3,13 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createRunner = createRunner;
4
4
  const LocalGradleRunner_1 = require("./LocalGradleRunner");
5
5
  const DockerGradleRunner_1 = require("./DockerGradleRunner");
6
- function createRunner(config) {
7
- switch (config.builder) {
6
+ function createRunner(builder) {
7
+ switch (builder) {
8
8
  case "docker":
9
- return new DockerGradleRunner_1.DockerGradleRunner(config.dockerImage);
9
+ return new DockerGradleRunner_1.DockerGradleRunner();
10
10
  case "local":
11
11
  return new LocalGradleRunner_1.LocalGradleRunner();
12
12
  default:
13
- throw new Error(`Unknown runner type: ${config.builder}`);
13
+ throw new Error(`Unknown builder: "${builder}". Only "docker" (and "local") are supported.`);
14
14
  }
15
15
  }