pakstr 0.7.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/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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
2
|
-
import java.util.Properties
|
|
3
2
|
import java.io.File
|
|
4
3
|
|
|
5
4
|
plugins {
|
|
@@ -7,49 +6,32 @@ plugins {
|
|
|
7
6
|
alias(libs.plugins.kotlin.compose)
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
val
|
|
11
|
-
|
|
12
|
-
Properties()
|
|
13
|
-
|
|
14
|
-
val keystorePath =
|
|
15
|
-
providers.gradleProperty(
|
|
16
|
-
"releaseKeystoreConfig"
|
|
17
|
-
).orNull
|
|
18
|
-
|
|
19
|
-
val keystorePropertiesFile =
|
|
20
|
-
keystorePath?.let { rootProject.file(it) }
|
|
21
|
-
if (keystorePropertiesFile?.exists() == true) {
|
|
22
|
-
|
|
23
|
-
keystorePropertiesFile.inputStream()
|
|
24
|
-
.use {
|
|
25
|
-
keystoreProperties.load(it)
|
|
26
|
-
}
|
|
9
|
+
val releaseRequested = gradle.startParameter.taskNames.any {
|
|
10
|
+
it.contains("release", ignoreCase = true)
|
|
27
11
|
}
|
|
28
12
|
|
|
13
|
+
fun requireReleaseSigningValue(name: String): String =
|
|
14
|
+
System.getenv(name)
|
|
15
|
+
?.takeIf { it.isNotEmpty() }
|
|
16
|
+
?: throw GradleException("Missing required release signing environment variable: $name")
|
|
17
|
+
|
|
29
18
|
android {
|
|
30
19
|
namespace = "com.pakstr.app"
|
|
31
20
|
compileSdk = 36
|
|
32
21
|
|
|
33
22
|
signingConfigs {
|
|
34
|
-
|
|
35
23
|
create("release") {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
keyAlias =
|
|
49
|
-
keystoreProperties["keyAlias"] as String
|
|
50
|
-
|
|
51
|
-
keyPassword =
|
|
52
|
-
keystoreProperties["keyPassword"] as String
|
|
24
|
+
if (releaseRequested) {
|
|
25
|
+
val keystorePath = requireReleaseSigningValue("PAKSTR_ANDROID_KEYSTORE_PATH")
|
|
26
|
+
val keystoreFile = File(keystorePath)
|
|
27
|
+
if (!keystoreFile.isFile) {
|
|
28
|
+
throw GradleException("Release signing keystore path is not a file")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
storeFile = keystoreFile
|
|
32
|
+
storePassword = requireReleaseSigningValue("PAKSTR_ANDROID_KEYSTORE_PASSWORD")
|
|
33
|
+
keyAlias = requireReleaseSigningValue("PAKSTR_ANDROID_KEY_ALIAS")
|
|
34
|
+
keyPassword = requireReleaseSigningValue("PAKSTR_ANDROID_KEY_PASSWORD")
|
|
53
35
|
}
|
|
54
36
|
}
|
|
55
37
|
}
|
|
@@ -65,25 +47,22 @@ android {
|
|
|
65
47
|
}
|
|
66
48
|
|
|
67
49
|
buildTypes {
|
|
68
|
-
|
|
69
50
|
debug {
|
|
70
51
|
applicationIdSuffix = ".debug"
|
|
71
52
|
versionNameSuffix = "-debug"
|
|
72
53
|
}
|
|
73
54
|
|
|
74
55
|
release {
|
|
75
|
-
signingConfig =
|
|
76
|
-
signingConfigs.getByName("release")
|
|
56
|
+
signingConfig = signingConfigs.getByName("release")
|
|
77
57
|
isMinifyEnabled = true
|
|
78
58
|
isShrinkResources = true
|
|
79
59
|
proguardFiles(
|
|
80
|
-
getDefaultProguardFile(
|
|
81
|
-
"proguard-android-optimize.txt"
|
|
82
|
-
),
|
|
60
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
83
61
|
"proguard-rules.pro"
|
|
84
62
|
)
|
|
85
63
|
}
|
|
86
64
|
}
|
|
65
|
+
|
|
87
66
|
compileOptions {
|
|
88
67
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
89
68
|
targetCompatibility = JavaVersion.VERSION_17
|
|
@@ -94,6 +73,7 @@ android {
|
|
|
94
73
|
buildConfig = true
|
|
95
74
|
}
|
|
96
75
|
}
|
|
76
|
+
|
|
97
77
|
tasks.withType<KotlinCompile>()
|
|
98
78
|
.configureEach {
|
|
99
79
|
compilerOptions {
|
|
@@ -121,9 +101,7 @@ dependencies {
|
|
|
121
101
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
|
122
102
|
implementation(libs.nanohttpd)
|
|
123
103
|
implementation(libs.androidx.appcompat)
|
|
124
|
-
|
|
125
104
|
implementation(libs.material)
|
|
126
105
|
implementation(libs.androidx.core.splashscreen)
|
|
127
|
-
|
|
128
106
|
implementation("org.bouncycastle:bcprov-jdk18on:1.78.1")
|
|
129
|
-
}
|
|
107
|
+
}
|
|
@@ -12,5 +12,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
|
12
12
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
|
13
13
|
# org.gradle.parallel=true
|
|
14
14
|
# Kotlin code style for this project: "official" or "obsolete":
|
|
15
|
-
kotlin.code.style=official
|
|
16
|
-
releaseKeystoreConfig=../../../keystore.properties
|
|
15
|
+
kotlin.code.style=official
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyReleaseApk = verifyReleaseApk;
|
|
4
|
+
exports.parseApkSignerFingerprints = parseApkSignerFingerprints;
|
|
5
|
+
exports.parseKeystoreFingerprints = parseKeystoreFingerprints;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const releaseSigning_1 = require("../core/releaseSigning");
|
|
8
|
+
function verifyReleaseApk(input) {
|
|
9
|
+
const signerResult = input.executor.run("apksigner", [
|
|
10
|
+
"verify",
|
|
11
|
+
"--print-certs",
|
|
12
|
+
input.artifactPath,
|
|
13
|
+
]);
|
|
14
|
+
const signerFingerprints = parseApkSignerFingerprints(signerResult.stdout);
|
|
15
|
+
if (signerFingerprints.length !== 1) {
|
|
16
|
+
throw new Error(`Release APK must have exactly one current signer; found ${signerFingerprints.length}`);
|
|
17
|
+
}
|
|
18
|
+
const keytoolEnv = {
|
|
19
|
+
[releaseSigning_1.PUBLIC_SIGNING_ENV.keystorePassword]: input.signing.storePassword,
|
|
20
|
+
};
|
|
21
|
+
const keytoolResult = input.executor.run("keytool", [
|
|
22
|
+
"-list",
|
|
23
|
+
"-rfc",
|
|
24
|
+
"-keystore",
|
|
25
|
+
input.toolKeystorePath ?? input.signing.keystorePath,
|
|
26
|
+
`-storepass:env`,
|
|
27
|
+
releaseSigning_1.PUBLIC_SIGNING_ENV.keystorePassword,
|
|
28
|
+
], keytoolEnv);
|
|
29
|
+
const keystoreFingerprints = parseKeystoreFingerprints(keytoolResult.stdout);
|
|
30
|
+
const signerFingerprint = signerFingerprints[0];
|
|
31
|
+
const matchingEntries = keystoreFingerprints.filter(fingerprint => fingerprint === signerFingerprint);
|
|
32
|
+
if (matchingEntries.length !== 1) {
|
|
33
|
+
throw new Error(`Release APK signer must match exactly one certificate in the supplied keystore; found ${matchingEntries.length}`);
|
|
34
|
+
}
|
|
35
|
+
const identityResult = input.executor.run("apkanalyzer", [
|
|
36
|
+
"manifest",
|
|
37
|
+
"application-id",
|
|
38
|
+
input.artifactPath,
|
|
39
|
+
]);
|
|
40
|
+
const applicationIds = identityResult.stdout
|
|
41
|
+
.split(/\r?\n/)
|
|
42
|
+
.map(value => value.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
if (applicationIds.length !== 1) {
|
|
45
|
+
throw new Error("Unable to derive one unambiguous Application ID from release APK");
|
|
46
|
+
}
|
|
47
|
+
const actualApplicationId = applicationIds[0];
|
|
48
|
+
if (actualApplicationId !== input.expectedApplicationId) {
|
|
49
|
+
throw new Error(`Release APK Application ID mismatch: expected ${input.expectedApplicationId}, received ${actualApplicationId}`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
signatureVerified: true,
|
|
53
|
+
signerMatched: true,
|
|
54
|
+
expectedApplicationId: input.expectedApplicationId,
|
|
55
|
+
actualApplicationId,
|
|
56
|
+
tools: {
|
|
57
|
+
signature: "apksigner",
|
|
58
|
+
signer: "keytool",
|
|
59
|
+
identity: "apkanalyzer",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseApkSignerFingerprints(output) {
|
|
64
|
+
const fingerprints = output
|
|
65
|
+
.split(/\r?\n/)
|
|
66
|
+
.map(line => line.match(/Signer #\d+ certificate SHA-256 digest:\s*([0-9a-fA-F]+)/)?.[1])
|
|
67
|
+
.filter((value) => value !== undefined)
|
|
68
|
+
.map(normalizeFingerprint);
|
|
69
|
+
return [...new Set(fingerprints)];
|
|
70
|
+
}
|
|
71
|
+
function parseKeystoreFingerprints(output) {
|
|
72
|
+
const certificates = output.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
|
|
73
|
+
if (!certificates?.length) {
|
|
74
|
+
throw new Error("No certificate-bearing entries found in supplied keystore");
|
|
75
|
+
}
|
|
76
|
+
return certificates.map(pem => {
|
|
77
|
+
const certificate = new crypto_1.X509Certificate(pem);
|
|
78
|
+
return (0, crypto_1.createHash)("sha256").update(certificate.raw).digest("hex");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function normalizeFingerprint(value) {
|
|
82
|
+
return value.replace(/[^0-9a-fA-F]/g, "").toLowerCase();
|
|
83
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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.createReleaseWorkspace = createReleaseWorkspace;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const template_1 = require("./template");
|
|
10
|
+
function createReleaseWorkspace(templateRoot) {
|
|
11
|
+
const workspaceRoot = path_1.default.join((0, template_1.getCacheDir)(), "workspaces");
|
|
12
|
+
fs_1.default.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
13
|
+
const workspaceParent = fs_1.default.mkdtempSync(path_1.default.join(workspaceRoot, "pakstr-release-workspace-"));
|
|
14
|
+
const androidRoot = path_1.default.join(workspaceParent, "android");
|
|
15
|
+
let cleaned = false;
|
|
16
|
+
try {
|
|
17
|
+
fs_1.default.chmodSync(workspaceParent, 0o700);
|
|
18
|
+
fs_1.default.cpSync(templateRoot, androidRoot, { recursive: true });
|
|
19
|
+
const gradlewPath = path_1.default.join(androidRoot, "gradlew");
|
|
20
|
+
if (fs_1.default.existsSync(gradlewPath)) {
|
|
21
|
+
fs_1.default.chmodSync(gradlewPath, 0o755);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
fs_1.default.rmSync(workspaceParent, { recursive: true, force: true });
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
androidRoot,
|
|
30
|
+
cleanup() {
|
|
31
|
+
if (cleaned)
|
|
32
|
+
return;
|
|
33
|
+
fs_1.default.rmSync(workspaceParent, {
|
|
34
|
+
recursive: true,
|
|
35
|
+
force: true,
|
|
36
|
+
maxRetries: 2,
|
|
37
|
+
});
|
|
38
|
+
if (fs_1.default.existsSync(workspaceParent)) {
|
|
39
|
+
throw new Error(`Failed to remove temporary release workspace: ${workspaceParent}`);
|
|
40
|
+
}
|
|
41
|
+
cleaned = true;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/android/template.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getCacheDir = getCacheDir;
|
|
6
7
|
exports.ensureTemplate = ensureTemplate;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -54,10 +55,14 @@ function fixGradlewPermissions(root) {
|
|
|
54
55
|
// -----------------------------
|
|
55
56
|
function createLocalProperties(root) {
|
|
56
57
|
const sdkPath = process.env.ANDROID_HOME ??
|
|
58
|
+
process.env.ANDROID_SDK_ROOT ??
|
|
57
59
|
path_1.default.join(os_1.default.homedir(), "Library", "Android", "sdk");
|
|
58
|
-
const content = `sdk.dir=${sdkPath}\n`;
|
|
59
60
|
const file = path_1.default.join(root, "local.properties");
|
|
60
|
-
fs_1.default.
|
|
61
|
+
if (!fs_1.default.existsSync(sdkPath)) {
|
|
62
|
+
fs_1.default.rmSync(file, { force: true });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
fs_1.default.writeFileSync(file, `sdk.dir=${sdkPath}\n`);
|
|
61
66
|
console.log("📍 local.properties created:", sdkPath);
|
|
62
67
|
}
|
|
63
68
|
async function ensureTemplate() {
|
package/dist/commands/build.js
CHANGED
|
@@ -8,140 +8,177 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const gradle_1 = require("../android/gradle");
|
|
11
|
-
const validateProject_1 = require("../core/validateProject");
|
|
12
|
-
const buildContext_1 = require("../core/buildContext");
|
|
13
|
-
const template_1 = require("../android/template");
|
|
14
|
-
const createRunner_1 = require("../runners/createRunner");
|
|
15
|
-
const config_1 = require("../core/config");
|
|
16
11
|
const branding_1 = require("../android/branding");
|
|
17
|
-
const version_1 = require("../android/version");
|
|
18
12
|
const icon_1 = require("../android/icon");
|
|
19
|
-
const splash_1 = require("../android/splash");
|
|
20
13
|
const permissions_1 = require("../android/permissions");
|
|
14
|
+
const releaseWorkspace_1 = require("../android/releaseWorkspace");
|
|
15
|
+
const splash_1 = require("../android/splash");
|
|
16
|
+
const template_1 = require("../android/template");
|
|
17
|
+
const version_1 = require("../android/version");
|
|
18
|
+
const buildContext_1 = require("../core/buildContext");
|
|
19
|
+
const config_1 = require("../core/config");
|
|
20
|
+
const releaseSigning_1 = require("../core/releaseSigning");
|
|
21
|
+
const validateProject_1 = require("../core/validateProject");
|
|
22
|
+
const createRunner_1 = require("../runners/createRunner");
|
|
21
23
|
const runtimeConfig_1 = require("../runtime/runtimeConfig");
|
|
22
24
|
async function buildCommand(args) {
|
|
23
25
|
const ctx = (0, buildContext_1.createBuildContext)(args);
|
|
24
26
|
const config = (0, config_1.loadConfig)(process.cwd());
|
|
25
27
|
const manifest = ctx.manifest;
|
|
26
|
-
console.log("📄 Manifest loaded");
|
|
27
|
-
const distPath = ctx.dist;
|
|
28
|
-
const androidRoot = await (0, template_1.ensureTemplate)();
|
|
29
28
|
const mode = ctx.mode;
|
|
30
|
-
const apkOutput = ctx.out;
|
|
31
|
-
const assetsTarget = path_1.default.join(androidRoot, "app/src/main/assets/www");
|
|
32
29
|
const appName = manifest?.app?.name || manifest?.appName || "Pakstr App";
|
|
30
|
+
const finalPath = path_1.default.resolve(ctx.out);
|
|
33
31
|
console.log("\n🚀 Pakstr CLI");
|
|
34
32
|
console.log("📦 App:", appName);
|
|
35
|
-
console.log("🌐 Web:",
|
|
36
|
-
console.log("📱 Out:",
|
|
37
|
-
console.log("📱 Android:", androidRoot);
|
|
33
|
+
console.log("🌐 Web:", ctx.dist);
|
|
34
|
+
console.log("📱 Out:", finalPath);
|
|
38
35
|
console.log("🏗️ Builder:", config.builder);
|
|
39
36
|
console.log("🚀 BUILD MODE:", mode.toUpperCase());
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
(0, validateProject_1.validateProject)(ctx.dist, manifest);
|
|
38
|
+
validateOutputParent(finalPath);
|
|
39
|
+
if (mode === "release") {
|
|
40
|
+
removeExistingOutput(finalPath);
|
|
41
|
+
}
|
|
42
|
+
const signingInput = mode === "release" ? (0, releaseSigning_1.readReleaseSigningInput)() : undefined;
|
|
43
|
+
const templateRoot = await (0, template_1.ensureTemplate)();
|
|
44
|
+
let workspace;
|
|
45
|
+
let signing;
|
|
46
|
+
let stagedPath;
|
|
47
|
+
let primaryError;
|
|
48
|
+
let cleanupError;
|
|
49
|
+
try {
|
|
50
|
+
const androidRoot = mode === "release"
|
|
51
|
+
? (workspace = (0, releaseWorkspace_1.createReleaseWorkspace)(templateRoot)).androidRoot
|
|
52
|
+
: templateRoot;
|
|
53
|
+
console.log("📱 Android:", androidRoot);
|
|
54
|
+
await prepareAndroidProject(androidRoot, ctx.dist, manifest, appName);
|
|
55
|
+
if (signingInput) {
|
|
56
|
+
signing = (0, releaseSigning_1.createReleaseSigningContext)(signingInput);
|
|
57
|
+
signingInput.keystoreBase64 = "";
|
|
58
|
+
}
|
|
59
|
+
console.log("\n⚙️ Running build runner...");
|
|
60
|
+
const result = await (0, createRunner_1.createRunner)(config).build({
|
|
61
|
+
androidRoot,
|
|
62
|
+
mode,
|
|
63
|
+
expectedApplicationId: manifest.appId,
|
|
64
|
+
signing,
|
|
65
|
+
});
|
|
66
|
+
if (mode === "release" && !result.verification) {
|
|
67
|
+
throw new Error("Release runner returned no verification evidence");
|
|
68
|
+
}
|
|
69
|
+
stagedPath = stageArtifact(result.artifactPath, finalPath);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
primaryError = error;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
signing?.cleanup();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
cleanupError = error;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
workspace?.cleanup();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
cleanupError ??= error;
|
|
85
|
+
}
|
|
86
|
+
if (primaryError || cleanupError) {
|
|
87
|
+
removeStagedArtifact(stagedPath);
|
|
88
|
+
removeExistingOutput(finalPath);
|
|
89
|
+
if (primaryError) {
|
|
90
|
+
if (cleanupError) {
|
|
91
|
+
console.error("Cleanup also failed:", safeErrorMessage(cleanupError));
|
|
92
|
+
}
|
|
93
|
+
throw primaryError;
|
|
94
|
+
}
|
|
95
|
+
throw cleanupError;
|
|
96
|
+
}
|
|
97
|
+
if (!stagedPath) {
|
|
98
|
+
throw new Error("Build completed without a staged APK");
|
|
99
|
+
}
|
|
100
|
+
publishArtifact(stagedPath, finalPath);
|
|
101
|
+
console.log("\n🚀 BUILD COMPLETE");
|
|
102
|
+
console.log("App:", appName);
|
|
103
|
+
console.log("Mode:", mode.toUpperCase());
|
|
104
|
+
console.log("📦 Output:", finalPath);
|
|
105
|
+
openFolder(finalPath);
|
|
106
|
+
}
|
|
107
|
+
async function prepareAndroidProject(androidRoot, distPath, manifest, appName) {
|
|
108
|
+
const assetsTarget = path_1.default.join(androidRoot, "app/src/main/assets/www");
|
|
44
109
|
if (!fs_1.default.existsSync(distPath)) {
|
|
45
|
-
throw new Error("
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// -----------------------------
|
|
50
|
-
console.log("\n📦 Copying web → Android assets...");
|
|
51
|
-
fs_1.default.rmSync(assetsTarget, {
|
|
52
|
-
recursive: true,
|
|
53
|
-
force: true,
|
|
54
|
-
});
|
|
55
|
-
fs_1.default.mkdirSync(assetsTarget, {
|
|
56
|
-
recursive: true,
|
|
57
|
-
});
|
|
110
|
+
throw new Error("Web assets not found");
|
|
111
|
+
}
|
|
112
|
+
fs_1.default.rmSync(assetsTarget, { recursive: true, force: true });
|
|
113
|
+
fs_1.default.mkdirSync(assetsTarget, { recursive: true });
|
|
58
114
|
copyFolder(distPath, assetsTarget);
|
|
59
|
-
console.log("📦 Web assets copied successfully");
|
|
60
115
|
(0, runtimeConfig_1.generateRuntimeConfig)(assetsTarget, manifest);
|
|
61
|
-
console.log("runtime files:", fs_1.default.readdirSync(assetsTarget)
|
|
62
|
-
.filter(f => f.includes("pakstr")));
|
|
63
|
-
console.log(fs_1.default.existsSync(path_1.default.join(assetsTarget, "pakstr-runtime.json")));
|
|
64
|
-
console.log(fs_1.default.readdirSync(assetsTarget));
|
|
65
|
-
// -----------------------------
|
|
66
|
-
// 3. PATCH ANDROID PROJECT
|
|
67
|
-
// -----------------------------
|
|
68
116
|
(0, gradle_1.patchGradle)(androidRoot, manifest);
|
|
69
|
-
console.log("⚙️ Gradle patched");
|
|
70
|
-
// -----------------------------
|
|
71
|
-
// 3.1 PATCH BRANDING
|
|
72
|
-
// -----------------------------
|
|
73
117
|
(0, branding_1.patchAppName)(androidRoot, appName);
|
|
74
118
|
(0, branding_1.patchPackageName)(androidRoot, manifest.appId);
|
|
75
119
|
(0, version_1.patchVersion)(androidRoot, manifest.versionCode, manifest.versionName);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await (0, icon_1.patchIcon)(androidRoot, iconPath, manifest.backgroundColor ?? "#FFFFFF");
|
|
120
|
+
if (manifest.ui?.icon) {
|
|
121
|
+
await (0, icon_1.patchIcon)(androidRoot, manifest.ui.icon, manifest.backgroundColor ?? "#FFFFFF");
|
|
79
122
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.log("SPLASH IMAGE:", splash.image);
|
|
83
|
-
console.log("SPLASH BACKGROUND:", splash.background);
|
|
84
|
-
await (0, splash_1.patchSplash)(androidRoot, splash.image, splash.background ?? "#FFFFFF");
|
|
123
|
+
if (manifest.ui?.splash) {
|
|
124
|
+
await (0, splash_1.patchSplash)(androidRoot, manifest.ui.splash.image, manifest.ui.splash.background ?? "#FFFFFF");
|
|
85
125
|
}
|
|
86
126
|
(0, permissions_1.patchPermissions)(androidRoot, manifest);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log("\n📦 APK GENERATED:");
|
|
97
|
-
console.log(apkPath);
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
console.error("❌ Build failed");
|
|
101
|
-
console.error(err);
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
// -----------------------------
|
|
105
|
-
// 5. COPY APK TO OUTPUT
|
|
106
|
-
// -----------------------------
|
|
107
|
-
const finalPath = path_1.default.resolve(apkOutput);
|
|
108
|
-
fs_1.default.mkdirSync(path_1.default.dirname(finalPath), {
|
|
109
|
-
recursive: true,
|
|
110
|
-
});
|
|
127
|
+
}
|
|
128
|
+
function validateOutputParent(finalPath) {
|
|
129
|
+
const parent = path_1.default.dirname(finalPath);
|
|
130
|
+
if (!fs_1.default.existsSync(parent) || !fs_1.default.lstatSync(parent).isDirectory()) {
|
|
131
|
+
throw new Error(`Output directory must already exist: ${parent}`);
|
|
132
|
+
}
|
|
133
|
+
if (fs_1.default.lstatSync(parent).isSymbolicLink()) {
|
|
134
|
+
throw new Error(`Output directory must not be a symbolic link: ${parent}`);
|
|
135
|
+
}
|
|
111
136
|
if (fs_1.default.existsSync(finalPath)) {
|
|
112
137
|
const stat = fs_1.default.lstatSync(finalPath);
|
|
113
|
-
if (stat.
|
|
114
|
-
|
|
115
|
-
recursive: true,
|
|
116
|
-
force: true,
|
|
117
|
-
});
|
|
138
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
139
|
+
throw new Error(`Output path must be a regular file: ${finalPath}`);
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
|
-
fs_1.default.copyFileSync(apkPath, finalPath);
|
|
121
|
-
console.log("\n📦 CLEAN OUTPUT:");
|
|
122
|
-
console.log(finalPath);
|
|
123
|
-
openFolder(finalPath);
|
|
124
|
-
// -----------------------------
|
|
125
|
-
// 6. FINAL OUTPUT
|
|
126
|
-
// -----------------------------
|
|
127
|
-
console.log("\n🚀 BUILD COMPLETE");
|
|
128
|
-
console.log("App:", appName);
|
|
129
|
-
console.log("Mode:", mode.toUpperCase());
|
|
130
142
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
function stageArtifact(artifactPath, finalPath) {
|
|
144
|
+
const parent = path_1.default.dirname(finalPath);
|
|
145
|
+
const stageDir = fs_1.default.mkdtempSync(path_1.default.join(parent, ".pakstr-stage-"));
|
|
146
|
+
const stagedPath = path_1.default.join(stageDir, "app.apk");
|
|
147
|
+
fs_1.default.copyFileSync(artifactPath, stagedPath, fs_1.default.constants.COPYFILE_EXCL);
|
|
148
|
+
if (fs_1.default.statSync(stagedPath).size !== fs_1.default.statSync(artifactPath).size) {
|
|
149
|
+
fs_1.default.rmSync(stageDir, { recursive: true, force: true });
|
|
150
|
+
throw new Error("Staged APK size does not match verified artifact");
|
|
151
|
+
}
|
|
152
|
+
return stagedPath;
|
|
153
|
+
}
|
|
154
|
+
function publishArtifact(stagedPath, finalPath) {
|
|
155
|
+
removeExistingOutput(finalPath);
|
|
156
|
+
fs_1.default.renameSync(stagedPath, finalPath);
|
|
157
|
+
fs_1.default.rmSync(path_1.default.dirname(stagedPath), { recursive: true, force: true });
|
|
158
|
+
if (!fs_1.default.existsSync(finalPath) || !fs_1.default.lstatSync(finalPath).isFile()) {
|
|
159
|
+
throw new Error("Final APK publication failed");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function removeExistingOutput(finalPath) {
|
|
163
|
+
if (!fs_1.default.existsSync(finalPath))
|
|
164
|
+
return;
|
|
165
|
+
const stat = fs_1.default.lstatSync(finalPath);
|
|
166
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
167
|
+
throw new Error(`Refusing to remove non-regular output path: ${finalPath}`);
|
|
168
|
+
}
|
|
169
|
+
fs_1.default.unlinkSync(finalPath);
|
|
170
|
+
}
|
|
171
|
+
function removeStagedArtifact(stagedPath) {
|
|
172
|
+
if (!stagedPath)
|
|
173
|
+
return;
|
|
174
|
+
fs_1.default.rmSync(path_1.default.dirname(stagedPath), { recursive: true, force: true });
|
|
175
|
+
}
|
|
134
176
|
function copyFolder(src, dest) {
|
|
135
|
-
const
|
|
136
|
-
withFileTypes: true,
|
|
137
|
-
});
|
|
138
|
-
for (const entry of entries) {
|
|
177
|
+
for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
|
|
139
178
|
const srcPath = path_1.default.join(src, entry.name);
|
|
140
179
|
const destPath = path_1.default.join(dest, entry.name);
|
|
141
180
|
if (entry.isDirectory()) {
|
|
142
|
-
fs_1.default.mkdirSync(destPath, {
|
|
143
|
-
recursive: true,
|
|
144
|
-
});
|
|
181
|
+
fs_1.default.mkdirSync(destPath, { recursive: true });
|
|
145
182
|
copyFolder(srcPath, destPath);
|
|
146
183
|
}
|
|
147
184
|
else {
|
|
@@ -149,17 +186,16 @@ function copyFolder(src, dest) {
|
|
|
149
186
|
}
|
|
150
187
|
}
|
|
151
188
|
}
|
|
152
|
-
// -----------------------------
|
|
153
|
-
// OPEN FOLDER (macOS)
|
|
154
|
-
// -----------------------------
|
|
155
189
|
function openFolder(filePath) {
|
|
156
|
-
if (process.platform !== "darwin")
|
|
190
|
+
if (process.platform !== "darwin" || process.env.CI)
|
|
157
191
|
return;
|
|
158
|
-
}
|
|
159
192
|
try {
|
|
160
|
-
(0, child_process_1.
|
|
193
|
+
(0, child_process_1.execFileSync)("open", [path_1.default.dirname(filePath)], { stdio: "ignore" });
|
|
161
194
|
}
|
|
162
195
|
catch {
|
|
163
|
-
//
|
|
196
|
+
// Opening the output folder is optional.
|
|
164
197
|
}
|
|
165
198
|
}
|
|
199
|
+
function safeErrorMessage(error) {
|
|
200
|
+
return error instanceof Error ? error.message : "unknown cleanup error";
|
|
201
|
+
}
|
package/dist/core/manifest.js
CHANGED
|
@@ -16,6 +16,16 @@ function loadManifest(path) {
|
|
|
16
16
|
function validateManifest(m) {
|
|
17
17
|
if (!m.appId)
|
|
18
18
|
throw new Error("❌ appId is required");
|
|
19
|
+
const appIdPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
20
|
+
if (m.appId.length < 3 ||
|
|
21
|
+
m.appId.length > 255 ||
|
|
22
|
+
!appIdPattern.test(m.appId)) {
|
|
23
|
+
throw new Error("❌ appId must be a lowercase reverse-domain Android Application ID");
|
|
24
|
+
}
|
|
25
|
+
const debugAppId = `${m.appId}.debug`;
|
|
26
|
+
if (debugAppId.length > 255 || !appIdPattern.test(debugAppId)) {
|
|
27
|
+
throw new Error("❌ appId is too long to derive the debug Application ID");
|
|
28
|
+
}
|
|
19
29
|
if (!m.appName)
|
|
20
30
|
throw new Error("❌ appName is required");
|
|
21
31
|
if (!m.versionName)
|
|
@@ -0,0 +1,134 @@
|
|
|
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.INTERNAL_KEYSTORE_PATH_ENV = exports.PUBLIC_SIGNING_ENV = void 0;
|
|
7
|
+
exports.readReleaseSigningInput = readReleaseSigningInput;
|
|
8
|
+
exports.createReleaseSigningContext = createReleaseSigningContext;
|
|
9
|
+
exports.removeSigningVariables = removeSigningVariables;
|
|
10
|
+
exports.createGradleSigningEnvironment = createGradleSigningEnvironment;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
exports.PUBLIC_SIGNING_ENV = {
|
|
15
|
+
keystoreBase64: "PAKSTR_ANDROID_KEYSTORE_BASE64",
|
|
16
|
+
keystorePassword: "PAKSTR_ANDROID_KEYSTORE_PASSWORD",
|
|
17
|
+
keyAlias: "PAKSTR_ANDROID_KEY_ALIAS",
|
|
18
|
+
keyPassword: "PAKSTR_ANDROID_KEY_PASSWORD",
|
|
19
|
+
};
|
|
20
|
+
exports.INTERNAL_KEYSTORE_PATH_ENV = "PAKSTR_ANDROID_KEYSTORE_PATH";
|
|
21
|
+
const MAX_KEYSTORE_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
23
|
+
function readReleaseSigningInput(env = process.env) {
|
|
24
|
+
const keystoreBase64 = requireValue(env, exports.PUBLIC_SIGNING_ENV.keystoreBase64);
|
|
25
|
+
const storePassword = requireValue(env, exports.PUBLIC_SIGNING_ENV.keystorePassword);
|
|
26
|
+
const keyAlias = requireValue(env, exports.PUBLIC_SIGNING_ENV.keyAlias);
|
|
27
|
+
const keyPassword = requireValue(env, exports.PUBLIC_SIGNING_ENV.keyPassword);
|
|
28
|
+
validateCanonicalBase64(keystoreBase64);
|
|
29
|
+
return {
|
|
30
|
+
keystoreBase64,
|
|
31
|
+
storePassword,
|
|
32
|
+
keyAlias,
|
|
33
|
+
keyPassword,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createReleaseSigningContext(input, temporaryRoot = os_1.default.tmpdir()) {
|
|
37
|
+
let decoded = decodeKeystore(input.keystoreBase64);
|
|
38
|
+
fs_1.default.mkdirSync(temporaryRoot, { recursive: true, mode: 0o700 });
|
|
39
|
+
const signingDir = fs_1.default.mkdtempSync(path_1.default.join(temporaryRoot, "pakstr-signing-"));
|
|
40
|
+
const keystorePath = path_1.default.join(signingDir, "release.keystore");
|
|
41
|
+
let cleaned = false;
|
|
42
|
+
try {
|
|
43
|
+
fs_1.default.chmodSync(signingDir, 0o700);
|
|
44
|
+
fs_1.default.writeFileSync(keystorePath, decoded, {
|
|
45
|
+
flag: "wx",
|
|
46
|
+
mode: 0o600,
|
|
47
|
+
});
|
|
48
|
+
verifyProtectedKeystore(signingDir, keystorePath, decoded.length);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
fs_1.default.rmSync(signingDir, { recursive: true, force: true });
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
decoded.fill(0);
|
|
56
|
+
decoded = Buffer.alloc(0);
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
keystorePath,
|
|
60
|
+
storePassword: input.storePassword,
|
|
61
|
+
keyAlias: input.keyAlias,
|
|
62
|
+
keyPassword: input.keyPassword,
|
|
63
|
+
cleanup() {
|
|
64
|
+
if (cleaned)
|
|
65
|
+
return;
|
|
66
|
+
fs_1.default.rmSync(signingDir, { recursive: true, force: true, maxRetries: 2 });
|
|
67
|
+
if (fs_1.default.existsSync(signingDir)) {
|
|
68
|
+
throw new Error(`Failed to remove temporary signing directory: ${signingDir}`);
|
|
69
|
+
}
|
|
70
|
+
cleaned = true;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function removeSigningVariables(env) {
|
|
75
|
+
const sanitized = { ...env };
|
|
76
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keystoreBase64];
|
|
77
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keystorePassword];
|
|
78
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keyAlias];
|
|
79
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keyPassword];
|
|
80
|
+
delete sanitized[exports.INTERNAL_KEYSTORE_PATH_ENV];
|
|
81
|
+
return sanitized;
|
|
82
|
+
}
|
|
83
|
+
function createGradleSigningEnvironment(context, env = process.env, keystorePath = context.keystorePath) {
|
|
84
|
+
return {
|
|
85
|
+
...removeSigningVariables(env),
|
|
86
|
+
[exports.INTERNAL_KEYSTORE_PATH_ENV]: keystorePath,
|
|
87
|
+
[exports.PUBLIC_SIGNING_ENV.keystorePassword]: context.storePassword,
|
|
88
|
+
[exports.PUBLIC_SIGNING_ENV.keyAlias]: context.keyAlias,
|
|
89
|
+
[exports.PUBLIC_SIGNING_ENV.keyPassword]: context.keyPassword,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function requireValue(env, name) {
|
|
93
|
+
const value = env[name];
|
|
94
|
+
if (value === undefined || value.length === 0) {
|
|
95
|
+
throw new Error(`${name} is required and must not be empty for release builds`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function validateCanonicalBase64(value) {
|
|
100
|
+
if (!CANONICAL_BASE64.test(value)) {
|
|
101
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} must be canonical single-line RFC 4648 base64`);
|
|
102
|
+
}
|
|
103
|
+
const decodedLength = Buffer.byteLength(value, "base64");
|
|
104
|
+
if (decodedLength === 0) {
|
|
105
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} decodes to an empty keystore`);
|
|
106
|
+
}
|
|
107
|
+
if (decodedLength > MAX_KEYSTORE_BYTES) {
|
|
108
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} exceeds the 16 MiB decoded size limit`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function decodeKeystore(value) {
|
|
112
|
+
const decoded = Buffer.from(value, "base64");
|
|
113
|
+
if (decoded.toString("base64") !== value) {
|
|
114
|
+
decoded.fill(0);
|
|
115
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} is not canonical base64`);
|
|
116
|
+
}
|
|
117
|
+
return decoded;
|
|
118
|
+
}
|
|
119
|
+
function verifyProtectedKeystore(signingDir, keystorePath, expectedSize) {
|
|
120
|
+
const dirStat = fs_1.default.lstatSync(signingDir);
|
|
121
|
+
const fileStat = fs_1.default.lstatSync(keystorePath);
|
|
122
|
+
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) {
|
|
123
|
+
throw new Error("Temporary signing directory is not a real directory");
|
|
124
|
+
}
|
|
125
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
126
|
+
throw new Error("Temporary keystore is not a regular file");
|
|
127
|
+
}
|
|
128
|
+
if ((dirStat.mode & 0o777) !== 0o700 || (fileStat.mode & 0o777) !== 0o600) {
|
|
129
|
+
throw new Error("Temporary signing storage does not have owner-only permissions");
|
|
130
|
+
}
|
|
131
|
+
if (fileStat.size !== expectedSize) {
|
|
132
|
+
throw new Error("Temporary keystore size does not match decoded input");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -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"
|