pakstr 0.6.0 → 0.8.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 +23 -45
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +72 -6
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsAccessManager.kt +14 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/SupportUnlock.kt +44 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/SupportUnlockGestureLayout.kt +87 -0
- package/android-template/app/src/main/res/layout/activity_layout.xml +2 -2
- package/android-template/app/src/main/res/values/strings.xml +1 -0
- package/android-template/gradle.properties +1 -2
- package/dist/android/releaseVerification.js +83 -0
- package/dist/android/releaseWorkspace.js +44 -0
- package/dist/android/template.js +7 -2
- package/dist/commands/build.js +144 -108
- package/dist/core/manifest.js +10 -0
- package/dist/core/releaseSigning.js +134 -0
- package/dist/runners/DockerGradleRunner.js +291 -37
- package/dist/runners/LocalGradleRunner.js +91 -30
- package/dist/runners/process.js +42 -0
- package/package.json +2 -1
|
@@ -4,57 +4,311 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DockerGradleRunner = void 0;
|
|
7
|
-
const child_process_1 = require("child_process");
|
|
8
|
-
const path_1 = __importDefault(require("path"));
|
|
9
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
10
|
+
const releaseVerification_1 = require("../android/releaseVerification");
|
|
11
|
+
const releaseSigning_1 = require("../core/releaseSigning");
|
|
12
|
+
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";
|
|
15
|
+
const CONTAINER_DEBUG_APK = "/workspace/app/build/outputs/apk/debug/app-debug.apk";
|
|
10
16
|
class DockerGradleRunner {
|
|
11
17
|
image;
|
|
18
|
+
execute;
|
|
12
19
|
constructor(image = process.env.PAKSTR_DOCKER_IMAGE ??
|
|
13
|
-
"git.nostrdev.com/stuff/pakstr/android-builder:latest") {
|
|
20
|
+
"git.nostrdev.com/stuff/pakstr/android-builder:latest", execute = process_1.runProcess) {
|
|
14
21
|
this.image = image;
|
|
22
|
+
this.execute = execute;
|
|
23
|
+
}
|
|
24
|
+
async build(request) {
|
|
25
|
+
this.checkDockerAvailable();
|
|
26
|
+
this.ensureDockerImage();
|
|
27
|
+
const signing = request.mode === "release" ? requireSigning(request.signing) : undefined;
|
|
28
|
+
const storage = this.createBuildStorage(request.androidRoot, signing);
|
|
29
|
+
let primaryError;
|
|
30
|
+
let result;
|
|
31
|
+
try {
|
|
32
|
+
result =
|
|
33
|
+
request.mode === "release"
|
|
34
|
+
? this.buildRelease(request, signing, storage)
|
|
35
|
+
: this.buildDebug(request, storage);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
primaryError = error;
|
|
39
|
+
}
|
|
40
|
+
let cleanupError;
|
|
41
|
+
try {
|
|
42
|
+
this.removeBuildStorage(storage);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
cleanupError = error;
|
|
46
|
+
}
|
|
47
|
+
if (primaryError) {
|
|
48
|
+
if (cleanupError) {
|
|
49
|
+
console.error("Docker signing storage cleanup also failed:", safeErrorMessage(cleanupError));
|
|
50
|
+
}
|
|
51
|
+
throw primaryError;
|
|
52
|
+
}
|
|
53
|
+
if (cleanupError)
|
|
54
|
+
throw cleanupError;
|
|
55
|
+
if (!result)
|
|
56
|
+
throw new Error("Docker build completed without a result");
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
buildDebug(request, storage) {
|
|
60
|
+
this.execute("docker", [
|
|
61
|
+
"run",
|
|
62
|
+
"--rm",
|
|
63
|
+
"-v",
|
|
64
|
+
`${storage.workspaceVolume}:/workspace`,
|
|
65
|
+
this.image,
|
|
66
|
+
"./gradlew",
|
|
67
|
+
"assembleDebug",
|
|
68
|
+
"-Dorg.gradle.vfs.watch=false",
|
|
69
|
+
], {
|
|
70
|
+
env: (0, releaseSigning_1.removeSigningVariables)(process.env),
|
|
71
|
+
streamOutput: true,
|
|
72
|
+
});
|
|
73
|
+
const artifactPath = hostArtifactPath(request.androidRoot, "debug");
|
|
74
|
+
this.copyFromVolume(storage.workspaceVolume, CONTAINER_DEBUG_APK, artifactPath);
|
|
75
|
+
return { artifactPath: requireApk(artifactPath) };
|
|
15
76
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
(0,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
];
|
|
89
|
+
this.execute("docker", [
|
|
90
|
+
"run",
|
|
91
|
+
"--rm",
|
|
92
|
+
"-v",
|
|
93
|
+
`${storage.workspaceVolume}:/workspace`,
|
|
94
|
+
"-v",
|
|
95
|
+
`${storage.signingVolume}:/run/secrets/pakstr:ro`,
|
|
96
|
+
...environmentNames.flatMap(name => ["--env", name]),
|
|
97
|
+
this.image,
|
|
98
|
+
"./gradlew",
|
|
99
|
+
"assembleRelease",
|
|
100
|
+
"--no-daemon",
|
|
101
|
+
"-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
|
+
expectedApplicationId: request.expectedApplicationId,
|
|
110
|
+
signing,
|
|
111
|
+
toolKeystorePath: CONTAINER_KEYSTORE_PATH,
|
|
112
|
+
executor: this.createVerificationExecutor(storage, signing),
|
|
27
113
|
});
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
114
|
+
const artifactPath = hostArtifactPath(request.androidRoot, "release");
|
|
115
|
+
this.copyFromVolume(storage.workspaceVolume, CONTAINER_RELEASE_APK, artifactPath);
|
|
116
|
+
return { artifactPath: requireApk(artifactPath), verification };
|
|
117
|
+
}
|
|
118
|
+
createBuildStorage(androidRoot, signing) {
|
|
119
|
+
const suffix = (0, crypto_1.randomUUID)();
|
|
120
|
+
const storage = {
|
|
121
|
+
workspaceVolume: `pakstr-workspace-${suffix}`,
|
|
122
|
+
signingVolume: signing ? `pakstr-signing-${suffix}` : undefined,
|
|
123
|
+
};
|
|
124
|
+
assertRegularAbsolutePath(androidRoot, "Android workspace");
|
|
125
|
+
if (signing) {
|
|
126
|
+
assertRegularAbsolutePath(signing.keystorePath, "Temporary keystore", true);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
this.execute("docker", ["volume", "create", storage.workspaceVolume]);
|
|
130
|
+
if (storage.signingVolume) {
|
|
131
|
+
this.execute("docker", ["volume", "create", storage.signingVolume]);
|
|
132
|
+
}
|
|
133
|
+
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
|
+
return storage;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
try {
|
|
141
|
+
this.removeBuildStorage(storage);
|
|
142
|
+
}
|
|
143
|
+
catch (cleanupError) {
|
|
144
|
+
console.error("Docker signing storage cleanup also failed:", safeErrorMessage(cleanupError));
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
copyDirectoryToVolume(source, volume) {
|
|
150
|
+
const container = `pakstr-workspace-copy-${(0, crypto_1.randomUUID)()}`;
|
|
151
|
+
try {
|
|
152
|
+
this.execute("docker", [
|
|
153
|
+
"create",
|
|
154
|
+
"--name",
|
|
155
|
+
container,
|
|
156
|
+
"-v",
|
|
157
|
+
`${volume}:/workspace`,
|
|
158
|
+
this.image,
|
|
159
|
+
"true",
|
|
160
|
+
]);
|
|
161
|
+
this.execute("docker", ["cp", `${source}${path_1.default.sep}.`, `${container}:/workspace`]);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
this.removeContainer(container);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
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
|
+
copyFromVolume(volume, source, destination) {
|
|
188
|
+
const container = `pakstr-output-copy-${(0, crypto_1.randomUUID)()}`;
|
|
189
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
190
|
+
fs_1.default.rmSync(destination, { force: true });
|
|
191
|
+
try {
|
|
192
|
+
this.execute("docker", [
|
|
193
|
+
"create",
|
|
194
|
+
"--name",
|
|
195
|
+
container,
|
|
196
|
+
"-v",
|
|
197
|
+
`${volume}:/workspace:ro`,
|
|
198
|
+
this.image,
|
|
199
|
+
"true",
|
|
200
|
+
]);
|
|
201
|
+
this.execute("docker", ["cp", `${container}:${source}`, destination]);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
this.removeContainer(container);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
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
|
+
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
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (failures.length) {
|
|
243
|
+
throw new Error(`Failed to remove temporary Docker volume(s): ${failures.join(", ")}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
removeContainer(container) {
|
|
247
|
+
try {
|
|
248
|
+
this.execute("docker", ["rm", "--force", container]);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// A failed docker create leaves no container to remove.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
checkDockerAvailable() {
|
|
255
|
+
try {
|
|
256
|
+
this.execute("docker", ["--version"]);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
throw new Error("Docker is not installed or not running. Please install Docker Desktop first.");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
ensureDockerImage() {
|
|
263
|
+
try {
|
|
264
|
+
this.execute("docker", ["image", "inspect", this.image]);
|
|
265
|
+
console.log(`🐳 Docker image ready: ${this.image}`);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
console.log(`⬇️ Pulling Docker image: ${this.image}`);
|
|
269
|
+
this.execute("docker", ["pull", this.image], { streamOutput: true });
|
|
31
270
|
}
|
|
32
|
-
return apkPath;
|
|
33
271
|
}
|
|
34
272
|
}
|
|
35
273
|
exports.DockerGradleRunner = DockerGradleRunner;
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
274
|
+
function hostArtifactPath(androidRoot, mode) {
|
|
275
|
+
return path_1.default.join(androidRoot, "app", "build", "outputs", "apk", mode, `app-${mode}.apk`);
|
|
276
|
+
}
|
|
277
|
+
function requireApk(artifactPath) {
|
|
278
|
+
if (!fs_1.default.existsSync(artifactPath) || !fs_1.default.lstatSync(artifactPath).isFile()) {
|
|
279
|
+
throw new Error(`APK not found after Docker build: ${artifactPath}`);
|
|
41
280
|
}
|
|
42
|
-
|
|
43
|
-
|
|
281
|
+
return artifactPath;
|
|
282
|
+
}
|
|
283
|
+
function requireSigning(signing) {
|
|
284
|
+
if (!signing) {
|
|
285
|
+
throw new Error("Release build requires an internal signing context");
|
|
44
286
|
}
|
|
287
|
+
return signing;
|
|
45
288
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
289
|
+
function signingSecrets(signing) {
|
|
290
|
+
return [
|
|
291
|
+
signing.storePassword,
|
|
292
|
+
signing.keyAlias,
|
|
293
|
+
signing.keyPassword,
|
|
294
|
+
signing.keystorePath,
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
function assertRegularAbsolutePath(candidate, label, requireFile = false) {
|
|
298
|
+
if (!path_1.default.isAbsolute(candidate)) {
|
|
299
|
+
throw new Error(`${label} path must be absolute`);
|
|
52
300
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
301
|
+
if (!fs_1.default.existsSync(candidate)) {
|
|
302
|
+
throw new Error(`${label} path does not exist`);
|
|
303
|
+
}
|
|
304
|
+
const stat = fs_1.default.lstatSync(candidate);
|
|
305
|
+
if (stat.isSymbolicLink()) {
|
|
306
|
+
throw new Error(`${label} path must not be a symbolic link`);
|
|
59
307
|
}
|
|
308
|
+
if (requireFile ? !stat.isFile() : !stat.isDirectory()) {
|
|
309
|
+
throw new Error(`${label} path has an unexpected filesystem type`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function safeErrorMessage(error) {
|
|
313
|
+
return error instanceof Error ? error.message : "unknown Docker cleanup error";
|
|
60
314
|
}
|
|
@@ -4,47 +4,108 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.LocalGradleRunner = void 0;
|
|
7
|
-
const child_process_1 = require("child_process");
|
|
8
|
-
const path_1 = __importDefault(require("path"));
|
|
9
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const releaseVerification_1 = require("../android/releaseVerification");
|
|
11
|
+
const releaseSigning_1 = require("../core/releaseSigning");
|
|
12
|
+
const process_1 = require("./process");
|
|
10
13
|
class LocalGradleRunner {
|
|
11
|
-
async build(
|
|
12
|
-
const
|
|
14
|
+
async build(request) {
|
|
15
|
+
const { androidRoot, mode } = request;
|
|
16
|
+
const gradlewPath = path_1.default.join(androidRoot, "gradlew");
|
|
17
|
+
if (!fs_1.default.existsSync(gradlewPath)) {
|
|
18
|
+
throw new Error("gradlew not found in Android project root");
|
|
19
|
+
}
|
|
13
20
|
console.log("⚙️ Local Gradle build");
|
|
14
21
|
console.log("📂 Android:", androidRoot);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
if (mode === "debug") {
|
|
23
|
+
(0, process_1.runProcess)(gradlewPath, ["assembleDebug"], {
|
|
24
|
+
cwd: androidRoot,
|
|
25
|
+
env: (0, releaseSigning_1.removeSigningVariables)(process.env),
|
|
26
|
+
streamOutput: true,
|
|
27
|
+
});
|
|
28
|
+
return { artifactPath: requireApk(androidRoot, "debug") };
|
|
18
29
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
(0,
|
|
30
|
+
const signing = requireSigning(request.signing);
|
|
31
|
+
const secrets = signingSecrets(signing);
|
|
32
|
+
const gradleEnv = (0, releaseSigning_1.createGradleSigningEnvironment)(signing);
|
|
33
|
+
(0, process_1.runProcess)(gradlewPath, ["assembleRelease", "--no-daemon"], {
|
|
22
34
|
cwd: androidRoot,
|
|
23
|
-
|
|
35
|
+
env: gradleEnv,
|
|
36
|
+
secrets,
|
|
37
|
+
streamOutput: true,
|
|
24
38
|
});
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
const artifactPath = requireApk(androidRoot, "release");
|
|
40
|
+
const executor = createLocalVerificationExecutor(signing);
|
|
41
|
+
const verification = (0, releaseVerification_1.verifyReleaseApk)({
|
|
42
|
+
artifactPath,
|
|
43
|
+
expectedApplicationId: request.expectedApplicationId,
|
|
44
|
+
signing,
|
|
45
|
+
executor,
|
|
46
|
+
});
|
|
47
|
+
return { artifactPath, verification };
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
exports.LocalGradleRunner = LocalGradleRunner;
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
function createLocalVerificationExecutor(signing) {
|
|
52
|
+
const androidHome = process.env.ANDROID_HOME ??
|
|
53
|
+
process.env.ANDROID_SDK_ROOT ??
|
|
54
|
+
path_1.default.join(os_1.default.homedir(), "Library", "Android", "sdk");
|
|
55
|
+
const secrets = signingSecrets(signing);
|
|
56
|
+
return {
|
|
57
|
+
run(tool, args, env = {}) {
|
|
58
|
+
const command = resolveTool(tool, androidHome);
|
|
59
|
+
return (0, process_1.runProcess)(command, args, {
|
|
60
|
+
env: { ...process.env, ...env },
|
|
61
|
+
secrets,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function resolveTool(tool, androidHome) {
|
|
67
|
+
if (tool === "keytool")
|
|
68
|
+
return "keytool";
|
|
69
|
+
if (tool === "apkanalyzer") {
|
|
70
|
+
const analyzer = path_1.default.join(androidHome, "cmdline-tools", "latest", "bin", "apkanalyzer");
|
|
71
|
+
if (!fs_1.default.existsSync(analyzer)) {
|
|
72
|
+
throw new Error(`Required Android verification tool not found: ${analyzer}`);
|
|
73
|
+
}
|
|
74
|
+
return analyzer;
|
|
75
|
+
}
|
|
76
|
+
const buildToolsDir = path_1.default.join(androidHome, "build-tools");
|
|
77
|
+
if (!fs_1.default.existsSync(buildToolsDir)) {
|
|
78
|
+
throw new Error(`Android build-tools directory not found: ${buildToolsDir}`);
|
|
79
|
+
}
|
|
80
|
+
const versions = fs_1.default
|
|
81
|
+
.readdirSync(buildToolsDir)
|
|
82
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
83
|
+
const signer = versions
|
|
84
|
+
.map(version => path_1.default.join(buildToolsDir, version, "apksigner"))
|
|
85
|
+
.find(candidate => fs_1.default.existsSync(candidate));
|
|
86
|
+
if (!signer) {
|
|
87
|
+
throw new Error(`Required Android verification tool apksigner not found under ${buildToolsDir}`);
|
|
38
88
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
return signer;
|
|
90
|
+
}
|
|
91
|
+
function requireApk(androidRoot, mode) {
|
|
92
|
+
const artifactPath = path_1.default.join(androidRoot, "app", "build", "outputs", "apk", mode, `app-${mode}.apk`);
|
|
93
|
+
if (!fs_1.default.existsSync(artifactPath) || !fs_1.default.lstatSync(artifactPath).isFile()) {
|
|
94
|
+
throw new Error(`APK not found after Gradle build: ${artifactPath}`);
|
|
95
|
+
}
|
|
96
|
+
return artifactPath;
|
|
97
|
+
}
|
|
98
|
+
function requireSigning(signing) {
|
|
99
|
+
if (!signing) {
|
|
100
|
+
throw new Error("Release build requires an internal signing context");
|
|
44
101
|
}
|
|
45
|
-
return
|
|
102
|
+
return signing;
|
|
46
103
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
104
|
+
function signingSecrets(signing) {
|
|
105
|
+
return [
|
|
106
|
+
signing.storePassword,
|
|
107
|
+
signing.keyAlias,
|
|
108
|
+
signing.keyPassword,
|
|
109
|
+
signing.keystorePath,
|
|
110
|
+
];
|
|
50
111
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runProcess = runProcess;
|
|
4
|
+
exports.redact = redact;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const MAX_DIAGNOSTIC_LENGTH = 32 * 1024;
|
|
7
|
+
function runProcess(command, args, options = {}) {
|
|
8
|
+
const result = (0, child_process_1.spawnSync)(command, args, {
|
|
9
|
+
cwd: options.cwd,
|
|
10
|
+
env: options.env,
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
const stdout = redact(result.stdout ?? "", options.secrets ?? []);
|
|
15
|
+
const stderr = redact(result.stderr ?? "", options.secrets ?? []);
|
|
16
|
+
if (options.streamOutput) {
|
|
17
|
+
if (stdout)
|
|
18
|
+
process.stdout.write(stdout);
|
|
19
|
+
if (stderr)
|
|
20
|
+
process.stderr.write(stderr);
|
|
21
|
+
}
|
|
22
|
+
if (result.error) {
|
|
23
|
+
throw new Error(`Failed to run ${command}: ${redact(result.error.message, options.secrets ?? [])}`);
|
|
24
|
+
}
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
const diagnostic = bounded(`${stderr}\n${stdout}`.trim());
|
|
27
|
+
const suffix = diagnostic ? `\n${diagnostic}` : "";
|
|
28
|
+
throw new Error(`${command} exited with status ${result.status}${result.signal ? ` (${result.signal})` : ""}${suffix}`);
|
|
29
|
+
}
|
|
30
|
+
return { stdout, stderr };
|
|
31
|
+
}
|
|
32
|
+
function redact(value, secrets) {
|
|
33
|
+
return secrets
|
|
34
|
+
.filter(secret => secret.length > 0)
|
|
35
|
+
.sort((a, b) => b.length - a.length)
|
|
36
|
+
.reduce((output, secret) => output.split(secret).join("[REDACTED]"), value);
|
|
37
|
+
}
|
|
38
|
+
function bounded(value) {
|
|
39
|
+
if (value.length <= MAX_DIAGNOSTIC_LENGTH)
|
|
40
|
+
return value;
|
|
41
|
+
return `[diagnostic truncated]\n${value.slice(-MAX_DIAGNOSTIC_LENGTH)}`;
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pakstr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "CLI for packaging Nostr web apps into Android APKs",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsc",
|
|
30
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
30
31
|
"start": "node dist/index.js",
|
|
31
32
|
"dev": "ts-node src/index.ts",
|
|
32
33
|
"pack:test": "npm pack"
|