pakstr 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/branding.js +14 -8
- package/dist/android/generatedProjectVerification.js +95 -0
- package/dist/android/gradle.js +61 -6
- package/dist/android/releaseVerification.js +103 -22
- package/dist/commands/build.js +14 -5
- package/dist/core/manifest.js +9 -0
- package/dist/runners/DockerGradleRunner.js +1 -1
- package/dist/runners/LocalGradleRunner.js +38 -15
- package/package.json +1 -1
- package/dist/android/version.js +0 -16
package/dist/android/branding.js
CHANGED
|
@@ -4,20 +4,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.patchAppName = patchAppName;
|
|
7
|
-
exports.patchPackageName = patchPackageName;
|
|
8
7
|
const fs_1 = __importDefault(require("fs"));
|
|
9
8
|
const path_1 = __importDefault(require("path"));
|
|
10
9
|
function patchAppName(androidRoot, appName) {
|
|
11
10
|
const stringsPath = path_1.default.join(androidRoot, "app", "src", "main", "res", "values", "strings.xml");
|
|
12
11
|
let xml = fs_1.default.readFileSync(stringsPath, "utf8");
|
|
13
|
-
|
|
12
|
+
const appNamePattern = /<string\s+name="app_name"[^>]*>[\s\S]*?<\/string>/g;
|
|
13
|
+
const matches = [...xml.matchAll(appNamePattern)];
|
|
14
|
+
if (matches.length !== 1) {
|
|
15
|
+
throw new Error(`Expected exactly one app_name string resource, found ${matches.length}`);
|
|
16
|
+
}
|
|
17
|
+
appNamePattern.lastIndex = 0;
|
|
18
|
+
xml = xml.replace(appNamePattern, `<string name="app_name">${escapeXmlText(appName)}</string>`);
|
|
14
19
|
fs_1.default.writeFileSync(stringsPath, xml);
|
|
15
20
|
console.log("✅ App name:", appName);
|
|
16
21
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
function escapeXmlText(value) {
|
|
23
|
+
return value
|
|
24
|
+
.replaceAll("&", "&")
|
|
25
|
+
.replaceAll("<", "<")
|
|
26
|
+
.replaceAll(">", ">")
|
|
27
|
+
.replaceAll('"', """)
|
|
28
|
+
.replaceAll("'", "'");
|
|
23
29
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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.verifyGeneratedAndroidProject = verifyGeneratedAndroidProject;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const gradle_1 = require("./gradle");
|
|
10
|
+
const INTERNAL_NAMESPACE = "com.pakstr.app";
|
|
11
|
+
const INTERNAL_PACKAGE_PATTERN = /^com\.pakstr\.app(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
12
|
+
const SOURCE_EXTENSIONS = new Set([".kt", ".java"]);
|
|
13
|
+
function verifyGeneratedAndroidProject(androidRoot, expected) {
|
|
14
|
+
const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
|
|
15
|
+
const gradle = (0, gradle_1.readAndroidGradleMetadata)(gradlePath);
|
|
16
|
+
assertEqual("applicationId", gradle.applicationId, expected.applicationId);
|
|
17
|
+
assertEqual("versionCode", gradle.versionCode, expected.versionCode);
|
|
18
|
+
assertEqual("versionName", gradle.versionName, expected.versionName);
|
|
19
|
+
assertEqual("namespace", gradle.namespace, INTERNAL_NAMESPACE);
|
|
20
|
+
const javaRoot = path_1.default.join(androidRoot, "app", "src", "main", "java");
|
|
21
|
+
const sourceRoot = path_1.default.join(javaRoot, "com", "pakstr", "app");
|
|
22
|
+
if (!fs_1.default.existsSync(sourceRoot) || !fs_1.default.lstatSync(sourceRoot).isDirectory()) {
|
|
23
|
+
throw new Error(`Expected Pakstr source root is missing: ${sourceRoot}`);
|
|
24
|
+
}
|
|
25
|
+
const sourceFiles = collectSourceFiles(javaRoot);
|
|
26
|
+
if (!sourceFiles.length) {
|
|
27
|
+
throw new Error(`No Kotlin or Java sources found under ${sourceRoot}`);
|
|
28
|
+
}
|
|
29
|
+
for (const sourceFile of sourceFiles) {
|
|
30
|
+
const declaration = readPackageDeclaration(sourceFile);
|
|
31
|
+
if (!INTERNAL_PACKAGE_PATTERN.test(declaration)) {
|
|
32
|
+
throw new Error(`Source package must remain ${INTERNAL_NAMESPACE} or a subpackage: ${sourceFile}`);
|
|
33
|
+
}
|
|
34
|
+
if (expected.applicationId !== INTERNAL_NAMESPACE &&
|
|
35
|
+
(declaration === expected.applicationId ||
|
|
36
|
+
declaration.startsWith(`${expected.applicationId}.`))) {
|
|
37
|
+
throw new Error(`Customer applicationId must not be used as a source package: ${sourceFile}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const stringsPath = path_1.default.join(androidRoot, "app", "src", "main", "res", "values", "strings.xml");
|
|
41
|
+
const appLabel = readSingleAppLabel(stringsPath);
|
|
42
|
+
assertEqual("app label", decodeXmlText(appLabel), expected.appLabel);
|
|
43
|
+
return {
|
|
44
|
+
applicationId: gradle.applicationId,
|
|
45
|
+
namespace: INTERNAL_NAMESPACE,
|
|
46
|
+
versionCode: gradle.versionCode,
|
|
47
|
+
versionName: gradle.versionName,
|
|
48
|
+
appLabel: expected.appLabel,
|
|
49
|
+
sourceFilesChecked: sourceFiles.length,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function collectSourceFiles(root) {
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const entry of fs_1.default.readdirSync(root, { withFileTypes: true })) {
|
|
55
|
+
const entryPath = path_1.default.join(root, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
result.push(...collectSourceFiles(entryPath));
|
|
58
|
+
}
|
|
59
|
+
else if (SOURCE_EXTENSIONS.has(path_1.default.extname(entry.name))) {
|
|
60
|
+
result.push(entryPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function readPackageDeclaration(sourceFile) {
|
|
66
|
+
const content = fs_1.default.readFileSync(sourceFile, "utf8");
|
|
67
|
+
const matches = [...content.matchAll(/^\s*package\s+([A-Za-z_][\w.]*)/gm)];
|
|
68
|
+
if (matches.length !== 1 || !matches[0][1]) {
|
|
69
|
+
throw new Error(`Expected exactly one package declaration in source file: ${sourceFile}`);
|
|
70
|
+
}
|
|
71
|
+
return matches[0][1];
|
|
72
|
+
}
|
|
73
|
+
function readSingleAppLabel(stringsPath) {
|
|
74
|
+
const xml = fs_1.default.readFileSync(stringsPath, "utf8");
|
|
75
|
+
const matches = [
|
|
76
|
+
...xml.matchAll(/<string\s+name="app_name"[^>]*>([\s\S]*?)<\/string>/g),
|
|
77
|
+
];
|
|
78
|
+
if (matches.length !== 1 || matches[0][1] === undefined) {
|
|
79
|
+
throw new Error(`Expected exactly one app_name string resource, found ${matches.length}`);
|
|
80
|
+
}
|
|
81
|
+
return matches[0][1];
|
|
82
|
+
}
|
|
83
|
+
function decodeXmlText(value) {
|
|
84
|
+
return value
|
|
85
|
+
.replaceAll("<", "<")
|
|
86
|
+
.replaceAll(">", ">")
|
|
87
|
+
.replaceAll(""", '"')
|
|
88
|
+
.replaceAll("'", "'")
|
|
89
|
+
.replaceAll("&", "&");
|
|
90
|
+
}
|
|
91
|
+
function assertEqual(field, actual, expected) {
|
|
92
|
+
if (actual !== expected) {
|
|
93
|
+
throw new Error(`Generated Android project ${field} mismatch: expected ${expected}, received ${actual}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/android/gradle.js
CHANGED
|
@@ -4,14 +4,69 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.patchGradle = patchGradle;
|
|
7
|
+
exports.readAndroidGradleMetadata = readAndroidGradleMetadata;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const ASSIGNMENTS = {
|
|
11
|
+
applicationId: /\bapplicationId\s*=\s*"([^"]*)"/g,
|
|
12
|
+
namespace: /\bnamespace\s*=\s*"([^"]*)"/g,
|
|
13
|
+
versionCode: /\bversionCode\s*=\s*(\d+)/g,
|
|
14
|
+
versionName: /\bversionName\s*=\s*"([^"]*)"/g,
|
|
15
|
+
};
|
|
9
16
|
function patchGradle(androidRoot, manifest) {
|
|
10
|
-
const gradlePath = path_1.default.join(androidRoot, "app
|
|
11
|
-
let content = fs_1.default.readFileSync(gradlePath, "
|
|
12
|
-
content = content
|
|
13
|
-
content = content.
|
|
14
|
-
content = content
|
|
17
|
+
const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
|
|
18
|
+
let content = fs_1.default.readFileSync(gradlePath, "utf8");
|
|
19
|
+
content = replaceSingleAssignment(content, "applicationId", ASSIGNMENTS.applicationId, `applicationId = ${quoteKotlinString(manifest.appId)}`);
|
|
20
|
+
content = replaceSingleAssignment(content, "versionCode", ASSIGNMENTS.versionCode, `versionCode = ${manifest.versionCode}`);
|
|
21
|
+
content = replaceSingleAssignment(content, "versionName", ASSIGNMENTS.versionName, `versionName = ${quoteKotlinString(manifest.versionName)}`);
|
|
15
22
|
fs_1.default.writeFileSync(gradlePath, content);
|
|
16
|
-
|
|
23
|
+
const metadata = readAndroidGradleMetadata(gradlePath);
|
|
24
|
+
if (metadata.applicationId !== manifest.appId) {
|
|
25
|
+
throw new Error("Generated Gradle applicationId does not match manifest.appId");
|
|
26
|
+
}
|
|
27
|
+
if (metadata.versionCode !== manifest.versionCode) {
|
|
28
|
+
throw new Error("Generated Gradle versionCode does not match manifest.versionCode");
|
|
29
|
+
}
|
|
30
|
+
if (metadata.versionName !== manifest.versionName) {
|
|
31
|
+
throw new Error("Generated Gradle versionName does not match manifest.versionName");
|
|
32
|
+
}
|
|
33
|
+
console.log("⚙️ Gradle identity and version patched");
|
|
34
|
+
return metadata;
|
|
35
|
+
}
|
|
36
|
+
function readAndroidGradleMetadata(gradlePath) {
|
|
37
|
+
const content = fs_1.default.readFileSync(gradlePath, "utf8");
|
|
38
|
+
return {
|
|
39
|
+
applicationId: readSingleAssignment(content, "applicationId", ASSIGNMENTS.applicationId),
|
|
40
|
+
namespace: readSingleAssignment(content, "namespace", ASSIGNMENTS.namespace),
|
|
41
|
+
versionCode: Number(readSingleAssignment(content, "versionCode", ASSIGNMENTS.versionCode)),
|
|
42
|
+
versionName: readSingleAssignment(content, "versionName", ASSIGNMENTS.versionName),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function replaceSingleAssignment(content, field, pattern, replacement) {
|
|
46
|
+
requireSingleMatch(content, field, pattern);
|
|
47
|
+
pattern.lastIndex = 0;
|
|
48
|
+
return content.replace(pattern, replacement);
|
|
49
|
+
}
|
|
50
|
+
function readSingleAssignment(content, field, pattern) {
|
|
51
|
+
requireSingleMatch(content, field, pattern);
|
|
52
|
+
pattern.lastIndex = 0;
|
|
53
|
+
const match = pattern.exec(content);
|
|
54
|
+
if (!match?.[1]) {
|
|
55
|
+
throw new Error(`Unable to read Gradle ${field} assignment`);
|
|
56
|
+
}
|
|
57
|
+
return match[1];
|
|
58
|
+
}
|
|
59
|
+
function requireSingleMatch(content, field, pattern) {
|
|
60
|
+
pattern.lastIndex = 0;
|
|
61
|
+
const matches = [...content.matchAll(pattern)];
|
|
62
|
+
pattern.lastIndex = 0;
|
|
63
|
+
if (matches.length !== 1) {
|
|
64
|
+
throw new Error(`Expected exactly one Kotlin DSL ${field} assignment, found ${matches.length}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function quoteKotlinString(value) {
|
|
68
|
+
if (/[\r\n"\\]/.test(value)) {
|
|
69
|
+
throw new Error("Gradle string values must not contain line breaks, quotes, or backslashes");
|
|
70
|
+
}
|
|
71
|
+
return `"${value}"`;
|
|
17
72
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.verifyReleaseApk = verifyReleaseApk;
|
|
4
4
|
exports.parseApkSignerFingerprints = parseApkSignerFingerprints;
|
|
5
5
|
exports.parseKeystoreFingerprints = parseKeystoreFingerprints;
|
|
6
|
+
exports.parseAaptBadging = parseAaptBadging;
|
|
6
7
|
const crypto_1 = require("crypto");
|
|
7
8
|
const releaseSigning_1 = require("../core/releaseSigning");
|
|
8
9
|
function verifyReleaseApk(input) {
|
|
@@ -23,40 +24,42 @@ function verifyReleaseApk(input) {
|
|
|
23
24
|
"-rfc",
|
|
24
25
|
"-keystore",
|
|
25
26
|
input.toolKeystorePath ?? input.signing.keystorePath,
|
|
26
|
-
|
|
27
|
+
"-storepass:env",
|
|
27
28
|
releaseSigning_1.PUBLIC_SIGNING_ENV.keystorePassword,
|
|
28
29
|
], keytoolEnv);
|
|
29
30
|
const keystoreFingerprints = parseKeystoreFingerprints(keytoolResult.stdout);
|
|
30
|
-
const
|
|
31
|
-
const matchingEntries = keystoreFingerprints.filter(fingerprint => fingerprint === signerFingerprint);
|
|
31
|
+
const matchingEntries = keystoreFingerprints.filter(fingerprint => fingerprint === signerFingerprints[0]);
|
|
32
32
|
if (matchingEntries.length !== 1) {
|
|
33
33
|
throw new Error(`Release APK signer must match exactly one certificate in the supplied keystore; found ${matchingEntries.length}`);
|
|
34
34
|
}
|
|
35
|
-
const
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
input
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (actualApplicationId !== input.expectedApplicationId) {
|
|
49
|
-
throw new Error(`Release APK Application ID mismatch: expected ${input.expectedApplicationId}, received ${actualApplicationId}`);
|
|
50
|
-
}
|
|
35
|
+
const analyzerMetadata = {
|
|
36
|
+
applicationId: readAnalyzerValue(input, "application-id"),
|
|
37
|
+
versionCode: parseVersionCode(readAnalyzerValue(input, "version-code")),
|
|
38
|
+
versionName: readAnalyzerValue(input, "version-name"),
|
|
39
|
+
};
|
|
40
|
+
const badging = parseAaptBadging(input.executor.run("aapt", ["dump", "badging", input.artifactPath]).stdout);
|
|
41
|
+
assertMetadataField("Application ID", analyzerMetadata.applicationId, input.expectedMetadata.applicationId);
|
|
42
|
+
assertMetadataField("versionCode", analyzerMetadata.versionCode, input.expectedMetadata.versionCode);
|
|
43
|
+
assertMetadataField("versionName", analyzerMetadata.versionName, input.expectedMetadata.versionName);
|
|
44
|
+
assertMetadataField("aapt Application ID", badging.applicationId, analyzerMetadata.applicationId);
|
|
45
|
+
assertMetadataField("aapt versionCode", badging.versionCode, analyzerMetadata.versionCode);
|
|
46
|
+
assertMetadataField("aapt versionName", badging.versionName, analyzerMetadata.versionName);
|
|
47
|
+
assertMetadataField("app label", badging.appLabel, input.expectedMetadata.appLabel);
|
|
51
48
|
return {
|
|
52
49
|
signatureVerified: true,
|
|
53
50
|
signerMatched: true,
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
expectedMetadata: input.expectedMetadata,
|
|
52
|
+
actualMetadata: {
|
|
53
|
+
applicationId: analyzerMetadata.applicationId,
|
|
54
|
+
versionCode: analyzerMetadata.versionCode,
|
|
55
|
+
versionName: analyzerMetadata.versionName,
|
|
56
|
+
appLabel: badging.appLabel,
|
|
57
|
+
},
|
|
56
58
|
tools: {
|
|
57
59
|
signature: "apksigner",
|
|
58
60
|
signer: "keytool",
|
|
59
|
-
|
|
61
|
+
manifest: "apkanalyzer",
|
|
62
|
+
badging: "aapt",
|
|
60
63
|
},
|
|
61
64
|
};
|
|
62
65
|
}
|
|
@@ -78,6 +81,84 @@ function parseKeystoreFingerprints(output) {
|
|
|
78
81
|
return (0, crypto_1.createHash)("sha256").update(certificate.raw).digest("hex");
|
|
79
82
|
});
|
|
80
83
|
}
|
|
84
|
+
function parseAaptBadging(output) {
|
|
85
|
+
const lines = output.split(/\r?\n/);
|
|
86
|
+
const packageLines = lines.filter(line => line.startsWith("package: "));
|
|
87
|
+
const labelLines = lines.filter(line => line.startsWith("application-label:"));
|
|
88
|
+
if (packageLines.length !== 1) {
|
|
89
|
+
throw new Error(`Expected exactly one aapt package line, found ${packageLines.length}`);
|
|
90
|
+
}
|
|
91
|
+
if (labelLines.length !== 1) {
|
|
92
|
+
throw new Error(`Expected exactly one default aapt application label, found ${labelLines.length}`);
|
|
93
|
+
}
|
|
94
|
+
const packageLine = packageLines[0];
|
|
95
|
+
const applicationId = readAaptAttribute(packageLine, "name");
|
|
96
|
+
const versionCode = parseVersionCode(readAaptAttribute(packageLine, "versionCode"));
|
|
97
|
+
const versionName = readAaptAttribute(packageLine, "versionName");
|
|
98
|
+
const appLabel = parseQuotedAaptValue(labelLines[0], "application-label");
|
|
99
|
+
return {
|
|
100
|
+
applicationId,
|
|
101
|
+
versionCode,
|
|
102
|
+
versionName,
|
|
103
|
+
appLabel,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function readAnalyzerValue(input, verb) {
|
|
107
|
+
const result = input.executor.run("apkanalyzer", [
|
|
108
|
+
"manifest",
|
|
109
|
+
verb,
|
|
110
|
+
input.artifactPath,
|
|
111
|
+
]);
|
|
112
|
+
const values = result.stdout
|
|
113
|
+
.split(/\r?\n/)
|
|
114
|
+
.map(value => value.trim())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
if (values.length !== 1) {
|
|
117
|
+
throw new Error(`Unable to derive one unambiguous ${verb} from release APK`);
|
|
118
|
+
}
|
|
119
|
+
return values[0];
|
|
120
|
+
}
|
|
121
|
+
function parseQuotedAaptValue(line, field) {
|
|
122
|
+
const pattern = new RegExp(`^${field}:'((?:\\\\.|[^'])*)'$`);
|
|
123
|
+
const match = pattern.exec(line);
|
|
124
|
+
if (!match || match[1] === undefined) {
|
|
125
|
+
throw new Error(`Unable to derive one unambiguous aapt ${field}`);
|
|
126
|
+
}
|
|
127
|
+
return decodeAaptEscapes(match[1]);
|
|
128
|
+
}
|
|
129
|
+
function readAaptAttribute(line, name) {
|
|
130
|
+
const pattern = new RegExp(`${name}='((?:\\\\.|[^'])*)'`);
|
|
131
|
+
const match = pattern.exec(line);
|
|
132
|
+
if (!match || match[1] === undefined) {
|
|
133
|
+
throw new Error(`Unable to derive one unambiguous aapt ${name}`);
|
|
134
|
+
}
|
|
135
|
+
return decodeAaptEscapes(match[1]);
|
|
136
|
+
}
|
|
137
|
+
function decodeAaptEscapes(value) {
|
|
138
|
+
return value.replace(/\\(.)/gs, (_match, escaped) => {
|
|
139
|
+
switch (escaped) {
|
|
140
|
+
case "n":
|
|
141
|
+
return "\n";
|
|
142
|
+
case "r":
|
|
143
|
+
return "\r";
|
|
144
|
+
case "t":
|
|
145
|
+
return "\t";
|
|
146
|
+
default:
|
|
147
|
+
return escaped;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function parseVersionCode(value) {
|
|
152
|
+
if (!/^\d+$/.test(value)) {
|
|
153
|
+
throw new Error(`APK versionCode is not an integer: ${value}`);
|
|
154
|
+
}
|
|
155
|
+
return Number(value);
|
|
156
|
+
}
|
|
157
|
+
function assertMetadataField(field, actual, expected) {
|
|
158
|
+
if (actual !== expected) {
|
|
159
|
+
throw new Error(`Release APK ${field} mismatch: expected ${expected}, received ${actual}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
81
162
|
function normalizeFingerprint(value) {
|
|
82
163
|
return value.replace(/[^0-9a-fA-F]/g, "").toLowerCase();
|
|
83
164
|
}
|
package/dist/commands/build.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.buildCommand = buildCommand;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
|
+
const generatedProjectVerification_1 = require("../android/generatedProjectVerification");
|
|
10
11
|
const gradle_1 = require("../android/gradle");
|
|
11
12
|
const branding_1 = require("../android/branding");
|
|
12
13
|
const icon_1 = require("../android/icon");
|
|
@@ -14,7 +15,6 @@ const permissions_1 = require("../android/permissions");
|
|
|
14
15
|
const releaseWorkspace_1 = require("../android/releaseWorkspace");
|
|
15
16
|
const splash_1 = require("../android/splash");
|
|
16
17
|
const template_1 = require("../android/template");
|
|
17
|
-
const version_1 = require("../android/version");
|
|
18
18
|
const buildContext_1 = require("../core/buildContext");
|
|
19
19
|
const config_1 = require("../core/config");
|
|
20
20
|
const releaseSigning_1 = require("../core/releaseSigning");
|
|
@@ -26,7 +26,7 @@ async function buildCommand(args) {
|
|
|
26
26
|
const config = (0, config_1.loadConfig)(process.cwd());
|
|
27
27
|
const manifest = ctx.manifest;
|
|
28
28
|
const mode = ctx.mode;
|
|
29
|
-
const appName = manifest
|
|
29
|
+
const appName = manifest.appName;
|
|
30
30
|
const finalPath = path_1.default.resolve(ctx.out);
|
|
31
31
|
console.log("\n🚀 Pakstr CLI");
|
|
32
32
|
console.log("📦 App:", appName);
|
|
@@ -60,7 +60,12 @@ async function buildCommand(args) {
|
|
|
60
60
|
const result = await (0, createRunner_1.createRunner)(config).build({
|
|
61
61
|
androidRoot,
|
|
62
62
|
mode,
|
|
63
|
-
|
|
63
|
+
expectedMetadata: {
|
|
64
|
+
applicationId: manifest.appId,
|
|
65
|
+
versionCode: manifest.versionCode,
|
|
66
|
+
versionName: manifest.versionName,
|
|
67
|
+
appLabel: appName,
|
|
68
|
+
},
|
|
64
69
|
signing,
|
|
65
70
|
});
|
|
66
71
|
if (mode === "release" && !result.verification) {
|
|
@@ -115,8 +120,6 @@ async function prepareAndroidProject(androidRoot, distPath, manifest, appName) {
|
|
|
115
120
|
(0, runtimeConfig_1.generateRuntimeConfig)(assetsTarget, manifest);
|
|
116
121
|
(0, gradle_1.patchGradle)(androidRoot, manifest);
|
|
117
122
|
(0, branding_1.patchAppName)(androidRoot, appName);
|
|
118
|
-
(0, branding_1.patchPackageName)(androidRoot, manifest.appId);
|
|
119
|
-
(0, version_1.patchVersion)(androidRoot, manifest.versionCode, manifest.versionName);
|
|
120
123
|
if (manifest.ui?.icon) {
|
|
121
124
|
await (0, icon_1.patchIcon)(androidRoot, manifest.ui.icon, manifest.backgroundColor ?? "#FFFFFF");
|
|
122
125
|
}
|
|
@@ -124,6 +127,12 @@ async function prepareAndroidProject(androidRoot, distPath, manifest, appName) {
|
|
|
124
127
|
await (0, splash_1.patchSplash)(androidRoot, manifest.ui.splash.image, manifest.ui.splash.background ?? "#FFFFFF");
|
|
125
128
|
}
|
|
126
129
|
(0, permissions_1.patchPermissions)(androidRoot, manifest);
|
|
130
|
+
(0, generatedProjectVerification_1.verifyGeneratedAndroidProject)(androidRoot, {
|
|
131
|
+
applicationId: manifest.appId,
|
|
132
|
+
versionCode: manifest.versionCode,
|
|
133
|
+
versionName: manifest.versionName,
|
|
134
|
+
appLabel: appName,
|
|
135
|
+
});
|
|
127
136
|
}
|
|
128
137
|
function validateOutputParent(finalPath) {
|
|
129
138
|
const parent = path_1.default.dirname(finalPath);
|
package/dist/core/manifest.js
CHANGED
|
@@ -28,10 +28,19 @@ function validateManifest(m) {
|
|
|
28
28
|
}
|
|
29
29
|
if (!m.appName)
|
|
30
30
|
throw new Error("❌ appName is required");
|
|
31
|
+
if (/[\r\n]/.test(m.appName)) {
|
|
32
|
+
throw new Error("❌ appName must not contain line breaks");
|
|
33
|
+
}
|
|
31
34
|
if (!m.versionName)
|
|
32
35
|
throw new Error("❌ versionName is required");
|
|
36
|
+
if (/[\r\n"\\]/.test(m.versionName)) {
|
|
37
|
+
throw new Error("❌ versionName must not contain line breaks, quotes, or backslashes");
|
|
38
|
+
}
|
|
33
39
|
if (m.versionCode === undefined)
|
|
34
40
|
throw new Error("❌ versionCode is required");
|
|
41
|
+
if (!Number.isInteger(m.versionCode) || m.versionCode <= 0) {
|
|
42
|
+
throw new Error("❌ versionCode must be a positive integer");
|
|
43
|
+
}
|
|
35
44
|
if (m.ui?.splash && !m.ui.splash.image) {
|
|
36
45
|
throw new Error("❌ splash.image is required");
|
|
37
46
|
}
|
|
@@ -106,7 +106,7 @@ class DockerGradleRunner {
|
|
|
106
106
|
});
|
|
107
107
|
const verification = (0, releaseVerification_1.verifyReleaseApk)({
|
|
108
108
|
artifactPath: CONTAINER_RELEASE_APK,
|
|
109
|
-
|
|
109
|
+
expectedMetadata: request.expectedMetadata,
|
|
110
110
|
signing,
|
|
111
111
|
toolKeystorePath: CONTAINER_KEYSTORE_PATH,
|
|
112
112
|
executor: this.createVerificationExecutor(storage, signing),
|
|
@@ -37,13 +37,26 @@ class LocalGradleRunner {
|
|
|
37
37
|
streamOutput: true,
|
|
38
38
|
});
|
|
39
39
|
const artifactPath = requireApk(androidRoot, "release");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
let verification;
|
|
41
|
+
try {
|
|
42
|
+
const executor = createLocalVerificationExecutor(signing);
|
|
43
|
+
verification = (0, releaseVerification_1.verifyReleaseApk)({
|
|
44
|
+
artifactPath,
|
|
45
|
+
expectedMetadata: request.expectedMetadata,
|
|
46
|
+
signing,
|
|
47
|
+
executor,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof Error &&
|
|
52
|
+
error.message.startsWith("Required Android verification tool not found")) {
|
|
53
|
+
console.warn("⚠️ Local release verification skipped: Android verification tools are missing.");
|
|
54
|
+
console.warn("ℹ️ Production releases should use CI Docker verification.");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
47
60
|
return { artifactPath, verification };
|
|
48
61
|
}
|
|
49
62
|
}
|
|
@@ -67,9 +80,18 @@ function resolveTool(tool, androidHome) {
|
|
|
67
80
|
if (tool === "keytool")
|
|
68
81
|
return "keytool";
|
|
69
82
|
if (tool === "apkanalyzer") {
|
|
70
|
-
const
|
|
71
|
-
if (!fs_1.default.existsSync(
|
|
72
|
-
throw new Error(`
|
|
83
|
+
const cmdlineTools = path_1.default.join(androidHome, "cmdline-tools");
|
|
84
|
+
if (!fs_1.default.existsSync(cmdlineTools)) {
|
|
85
|
+
throw new Error(`Android cmdline-tools directory not found: ${cmdlineTools}`);
|
|
86
|
+
}
|
|
87
|
+
const versions = fs_1.default
|
|
88
|
+
.readdirSync(cmdlineTools)
|
|
89
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
90
|
+
const analyzer = versions
|
|
91
|
+
.map(version => path_1.default.join(cmdlineTools, version, "bin", "apkanalyzer"))
|
|
92
|
+
.find(candidate => fs_1.default.existsSync(candidate));
|
|
93
|
+
if (!analyzer) {
|
|
94
|
+
throw new Error(`Required Android verification tool not found under ${cmdlineTools}`);
|
|
73
95
|
}
|
|
74
96
|
return analyzer;
|
|
75
97
|
}
|
|
@@ -80,13 +102,14 @@ function resolveTool(tool, androidHome) {
|
|
|
80
102
|
const versions = fs_1.default
|
|
81
103
|
.readdirSync(buildToolsDir)
|
|
82
104
|
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
83
|
-
const
|
|
84
|
-
|
|
105
|
+
const executable = tool === "aapt" ? "aapt" : "apksigner";
|
|
106
|
+
const resolved = versions
|
|
107
|
+
.map(version => path_1.default.join(buildToolsDir, version, executable))
|
|
85
108
|
.find(candidate => fs_1.default.existsSync(candidate));
|
|
86
|
-
if (!
|
|
87
|
-
throw new Error(`Required Android verification tool
|
|
109
|
+
if (!resolved) {
|
|
110
|
+
throw new Error(`Required Android verification tool ${executable} not found under ${buildToolsDir}`);
|
|
88
111
|
}
|
|
89
|
-
return
|
|
112
|
+
return resolved;
|
|
90
113
|
}
|
|
91
114
|
function requireApk(androidRoot, mode) {
|
|
92
115
|
const artifactPath = path_1.default.join(androidRoot, "app", "build", "outputs", "apk", mode, `app-${mode}.apk`);
|
package/package.json
CHANGED
package/dist/android/version.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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.patchVersion = patchVersion;
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const path_1 = __importDefault(require("path"));
|
|
9
|
-
function patchVersion(androidRoot, versionCode, versionName) {
|
|
10
|
-
const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
|
|
11
|
-
let gradle = fs_1.default.readFileSync(gradlePath, "utf8");
|
|
12
|
-
gradle = gradle.replace(/versionCode\s*=\s*[^\r\n]+/, `versionCode = ${versionCode}`);
|
|
13
|
-
gradle = gradle.replace(/versionName\s*=\s*"[^"]+"/, `versionName = "${versionName}"`);
|
|
14
|
-
fs_1.default.writeFileSync(gradlePath, gradle);
|
|
15
|
-
console.log("✅ Version patched:", versionCode, versionName);
|
|
16
|
-
}
|