pakstr 0.8.6 → 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.
- package/android-template/app/build.gradle.kts +11 -3
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +2 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/DebugReportExporter.kt +1 -1
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsActivity.kt +1 -1
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeviceDeveloperState.kt +23 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/WebViewDebugManager.kt +31 -4
- package/android-template/app/src/test/java/com/pakstr/app/debug/DeviceDeveloperStateTest.kt +21 -0
- package/android-template/app/src/test/java/com/pakstr/app/debug/WebViewDebugManagerTest.kt +84 -0
- package/bin/cli.js +11 -6
- package/dist/cli.js +53 -16
- package/dist/commands/build.js +42 -114
- package/dist/commands/init.js +191 -0
- package/dist/commands/publish.js +137 -0
- package/dist/commands/run.js +95 -0
- package/dist/commands/sign.js +41 -0
- package/dist/core/androidProject.js +70 -0
- package/dist/core/appIdentity.js +7 -0
- package/dist/core/blossom.js +122 -0
- package/dist/core/dotenv.js +51 -0
- package/dist/core/nostr.js +153 -0
- package/dist/core/pakstrConfig.js +206 -0
- package/dist/core/zapStore.js +221 -0
- package/dist/runners/DockerGradleRunner.js +38 -128
- package/dist/runners/DockerSignRunner.js +182 -0
- package/dist/runners/LocalGradleRunner.js +26 -129
- package/dist/runners/createRunner.js +4 -4
- package/package.json +7 -1
- package/dist/android/releaseVerification.js +0 -164
- package/dist/config.js +0 -4
- package/dist/core/buildContext.js +0 -102
- package/dist/core/config.js +0 -24
- package/dist/core/copyFile.js +0 -17
- package/dist/core/manifest.js +0 -47
- package/dist/core/releaseSigning.js +0 -134
- package/dist/core/resolveProjectInputs.js +0 -20
- package/dist/core/validateProject.js +0 -21
- package/dist/core/zeroConfig.js +0 -97
- package/dist/runtime/runtimeConfig.js +0 -22
|
@@ -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
|
|
14
|
-
const
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
243
|
-
throw new Error(`Failed to remove temporary Docker volume
|
|
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.
|
|
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,
|
|
275
|
-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
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
|
|
41
|
-
secrets,
|
|
35
|
+
env,
|
|
42
36
|
streamOutput: true,
|
|
43
37
|
});
|
|
44
|
-
const artifactPath = requireApk(androidRoot, "release");
|
|
45
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
7
|
-
switch (
|
|
6
|
+
function createRunner(builder) {
|
|
7
|
+
switch (builder) {
|
|
8
8
|
case "docker":
|
|
9
|
-
return new DockerGradleRunner_1.DockerGradleRunner(
|
|
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
|
|
13
|
+
throw new Error(`Unknown builder: "${builder}". Only "docker" (and "local") are supported.`);
|
|
14
14
|
}
|
|
15
15
|
}
|