pakstr 0.7.0 → 0.8.1

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.
@@ -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 keystoreProperties =
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
- if (keystorePropertiesFile?.exists() == true) {
38
-
39
- storeFile =
40
- File(
41
- keystorePropertiesFile.parentFile,
42
- keystoreProperties["storeFile"] as String
43
- )
44
-
45
- storePassword =
46
- keystoreProperties["storePassword"] as String
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
@@ -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
- xml = xml.replace(/<string name="app_name">.*?<\/string>/, `<string name="app_name">${appName}</string>`);
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 patchPackageName(androidRoot, packageName) {
18
- const gradlePath = path_1.default.join(androidRoot, "app", "build.gradle.kts");
19
- let gradle = fs_1.default.readFileSync(gradlePath, "utf8");
20
- gradle = gradle.replace(/applicationId\s*=\s*"[^"]+"/, `applicationId = "${packageName}"`);
21
- fs_1.default.writeFileSync(gradlePath, gradle);
22
- console.log(" ApplicationId:", packageName);
22
+ function escapeXmlText(value) {
23
+ return value
24
+ .replaceAll("&", "&amp;")
25
+ .replaceAll("<", "&lt;")
26
+ .replaceAll(">", "&gt;")
27
+ .replaceAll('"', "&quot;")
28
+ .replaceAll("'", "&apos;");
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("&lt;", "<")
86
+ .replaceAll("&gt;", ">")
87
+ .replaceAll("&quot;", '"')
88
+ .replaceAll("&apos;", "'")
89
+ .replaceAll("&amp;", "&");
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
+ }
@@ -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/build.gradle.kts");
11
- let content = fs_1.default.readFileSync(gradlePath, "utf-8");
12
- content = content.replace(/applicationId\s+".*"/, `applicationId "${manifest.appId}"`);
13
- content = content.replace(/versionCode\s+\d+/, `versionCode ${manifest.versionCode}`);
14
- content = content.replace(/versionName\s+".*"/, `versionName "${manifest.versionName}"`);
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
- console.log("⚙️ Gradle patched");
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
  }
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyReleaseApk = verifyReleaseApk;
4
+ exports.parseApkSignerFingerprints = parseApkSignerFingerprints;
5
+ exports.parseKeystoreFingerprints = parseKeystoreFingerprints;
6
+ exports.parseAaptBadging = parseAaptBadging;
7
+ const crypto_1 = require("crypto");
8
+ const releaseSigning_1 = require("../core/releaseSigning");
9
+ function verifyReleaseApk(input) {
10
+ const signerResult = input.executor.run("apksigner", [
11
+ "verify",
12
+ "--print-certs",
13
+ input.artifactPath,
14
+ ]);
15
+ const signerFingerprints = parseApkSignerFingerprints(signerResult.stdout);
16
+ if (signerFingerprints.length !== 1) {
17
+ throw new Error(`Release APK must have exactly one current signer; found ${signerFingerprints.length}`);
18
+ }
19
+ const keytoolEnv = {
20
+ [releaseSigning_1.PUBLIC_SIGNING_ENV.keystorePassword]: input.signing.storePassword,
21
+ };
22
+ const keytoolResult = input.executor.run("keytool", [
23
+ "-list",
24
+ "-rfc",
25
+ "-keystore",
26
+ input.toolKeystorePath ?? input.signing.keystorePath,
27
+ "-storepass:env",
28
+ releaseSigning_1.PUBLIC_SIGNING_ENV.keystorePassword,
29
+ ], keytoolEnv);
30
+ const keystoreFingerprints = parseKeystoreFingerprints(keytoolResult.stdout);
31
+ const matchingEntries = keystoreFingerprints.filter(fingerprint => fingerprint === signerFingerprints[0]);
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 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);
48
+ return {
49
+ signatureVerified: true,
50
+ signerMatched: true,
51
+ expectedMetadata: input.expectedMetadata,
52
+ actualMetadata: {
53
+ applicationId: analyzerMetadata.applicationId,
54
+ versionCode: analyzerMetadata.versionCode,
55
+ versionName: analyzerMetadata.versionName,
56
+ appLabel: badging.appLabel,
57
+ },
58
+ tools: {
59
+ signature: "apksigner",
60
+ signer: "keytool",
61
+ manifest: "apkanalyzer",
62
+ badging: "aapt",
63
+ },
64
+ };
65
+ }
66
+ function parseApkSignerFingerprints(output) {
67
+ const fingerprints = output
68
+ .split(/\r?\n/)
69
+ .map(line => line.match(/Signer #\d+ certificate SHA-256 digest:\s*([0-9a-fA-F]+)/)?.[1])
70
+ .filter((value) => value !== undefined)
71
+ .map(normalizeFingerprint);
72
+ return [...new Set(fingerprints)];
73
+ }
74
+ function parseKeystoreFingerprints(output) {
75
+ const certificates = output.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
76
+ if (!certificates?.length) {
77
+ throw new Error("No certificate-bearing entries found in supplied keystore");
78
+ }
79
+ return certificates.map(pem => {
80
+ const certificate = new crypto_1.X509Certificate(pem);
81
+ return (0, crypto_1.createHash)("sha256").update(certificate.raw).digest("hex");
82
+ });
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
+ }
162
+ function normalizeFingerprint(value) {
163
+ return value.replace(/[^0-9a-fA-F]/g, "").toLowerCase();
164
+ }
@@ -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
+ }
@@ -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.writeFileSync(file, content);
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() {