pakstr 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android-template/app/build.gradle.kts +23 -45
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +72 -6
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsAccessManager.kt +14 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/SupportUnlock.kt +44 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/SupportUnlockGestureLayout.kt +87 -0
- package/android-template/app/src/main/res/layout/activity_layout.xml +2 -2
- package/android-template/app/src/main/res/values/strings.xml +1 -0
- package/android-template/gradle.properties +1 -2
- package/dist/android/releaseVerification.js +83 -0
- package/dist/android/releaseWorkspace.js +44 -0
- package/dist/android/template.js +7 -2
- package/dist/commands/build.js +144 -108
- package/dist/core/manifest.js +10 -0
- package/dist/core/releaseSigning.js +134 -0
- package/dist/runners/DockerGradleRunner.js +291 -37
- package/dist/runners/LocalGradleRunner.js +91 -30
- package/dist/runners/process.js +42 -0
- package/package.json +2 -1
|
@@ -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
|
+
}
|
|
@@ -3,11 +3,14 @@ package com.pakstr.app
|
|
|
3
3
|
import android.Manifest
|
|
4
4
|
import android.content.Intent
|
|
5
5
|
import android.os.Bundle
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
6
8
|
|
|
7
9
|
import android.view.View
|
|
8
10
|
import android.webkit.PermissionRequest
|
|
9
11
|
import android.webkit.WebView
|
|
10
12
|
import android.widget.FrameLayout
|
|
13
|
+
import android.widget.Toast
|
|
11
14
|
import androidx.activity.OnBackPressedCallback
|
|
12
15
|
import androidx.activity.enableEdgeToEdge
|
|
13
16
|
import androidx.activity.result.ActivityResultLauncher
|
|
@@ -22,8 +25,11 @@ import com.pakstr.app.debug.AppDebugLogger
|
|
|
22
25
|
import com.pakstr.app.debug.CrashHandler
|
|
23
26
|
|
|
24
27
|
import com.pakstr.app.debug.DebugReportShare
|
|
28
|
+
import com.pakstr.app.debug.DeveloperToolsAccessManager
|
|
25
29
|
import com.pakstr.app.debug.DeveloperToolsActivity
|
|
26
30
|
import com.pakstr.app.debug.DeveloperToolsManager
|
|
31
|
+
import com.pakstr.app.debug.SupportUnlock
|
|
32
|
+
import com.pakstr.app.debug.SupportUnlockGestureLayout
|
|
27
33
|
import com.pakstr.app.debug.WebViewDebugManager
|
|
28
34
|
|
|
29
35
|
import com.pakstr.app.permissions.PermissionManager
|
|
@@ -46,6 +52,12 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
46
52
|
ActivityResultLauncher<Intent>
|
|
47
53
|
|
|
48
54
|
private lateinit var fab: FloatingActionButton
|
|
55
|
+
private lateinit var supportUnlockGestureLayout: SupportUnlockGestureLayout
|
|
56
|
+
|
|
57
|
+
private val diagnosticsHandler = Handler(Looper.getMainLooper())
|
|
58
|
+
private val diagnosticsExpiryCheck = Runnable {
|
|
59
|
+
showDebugFab()
|
|
60
|
+
}
|
|
49
61
|
|
|
50
62
|
override fun onCreate(
|
|
51
63
|
|
|
@@ -92,6 +104,7 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
92
104
|
)
|
|
93
105
|
|
|
94
106
|
initViews()
|
|
107
|
+
setupSupportUnlockGesture()
|
|
95
108
|
showDebugFab()
|
|
96
109
|
|
|
97
110
|
amberLauncher =
|
|
@@ -151,33 +164,61 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
private fun showDebugFab() {
|
|
154
|
-
|
|
167
|
+
diagnosticsHandler.removeCallbacks(diagnosticsExpiryCheck)
|
|
168
|
+
|
|
169
|
+
if (DeveloperToolsAccessManager.canOpenDeveloperTools(this)) {
|
|
155
170
|
|
|
156
171
|
fab.visibility = View.VISIBLE
|
|
157
172
|
|
|
158
173
|
fab.setOnClickListener {
|
|
174
|
+
if (DeveloperToolsAccessManager.canOpenDeveloperTools(this)) {
|
|
175
|
+
startActivity(
|
|
159
176
|
|
|
160
|
-
|
|
177
|
+
Intent(
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
this,
|
|
163
180
|
|
|
164
|
-
|
|
181
|
+
DeveloperToolsActivity::class.java
|
|
165
182
|
|
|
166
|
-
|
|
183
|
+
)
|
|
167
184
|
|
|
168
185
|
)
|
|
186
|
+
} else {
|
|
187
|
+
showDebugFab()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
169
190
|
|
|
191
|
+
val remainingDuration = SupportUnlock.remainingDuration(this)
|
|
192
|
+
if (!BuildConfig.DEBUG &&
|
|
193
|
+
!RuntimeConfig.debugEnabled(this) &&
|
|
194
|
+
remainingDuration > 0L
|
|
195
|
+
) {
|
|
196
|
+
diagnosticsHandler.postDelayed(
|
|
197
|
+
diagnosticsExpiryCheck,
|
|
198
|
+
remainingDuration
|
|
170
199
|
)
|
|
171
|
-
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
} else {
|
|
175
203
|
|
|
176
204
|
fab.visibility = View.GONE
|
|
205
|
+
fab.setOnClickListener(null)
|
|
177
206
|
|
|
178
207
|
}
|
|
179
208
|
}
|
|
180
209
|
|
|
210
|
+
private fun setupSupportUnlockGesture() {
|
|
211
|
+
supportUnlockGestureLayout.onUnlock = {
|
|
212
|
+
SupportUnlock.unlock(this)
|
|
213
|
+
Toast.makeText(
|
|
214
|
+
this,
|
|
215
|
+
R.string.diagnostics_mode_enabled,
|
|
216
|
+
Toast.LENGTH_LONG
|
|
217
|
+
).show()
|
|
218
|
+
showDebugFab()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
181
222
|
private fun setupBackNavigation() {
|
|
182
223
|
onBackPressedDispatcher.addCallback(
|
|
183
224
|
this,
|
|
@@ -220,6 +261,9 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
220
261
|
container =
|
|
221
262
|
findViewById(R.id.mainContainer)
|
|
222
263
|
|
|
264
|
+
supportUnlockGestureLayout =
|
|
265
|
+
container as SupportUnlockGestureLayout
|
|
266
|
+
|
|
223
267
|
webView =
|
|
224
268
|
findViewById(R.id.webView)
|
|
225
269
|
|
|
@@ -243,7 +287,29 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
243
287
|
)
|
|
244
288
|
}
|
|
245
289
|
|
|
290
|
+
override fun onResume() {
|
|
291
|
+
super.onResume()
|
|
292
|
+
|
|
293
|
+
if (::fab.isInitialized) {
|
|
294
|
+
showDebugFab()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
override fun onStop() {
|
|
299
|
+
if (::supportUnlockGestureLayout.isInitialized) {
|
|
300
|
+
supportUnlockGestureLayout.cancelHold()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
super.onStop()
|
|
304
|
+
}
|
|
305
|
+
|
|
246
306
|
override fun onDestroy() {
|
|
307
|
+
diagnosticsHandler.removeCallbacks(diagnosticsExpiryCheck)
|
|
308
|
+
|
|
309
|
+
if (::supportUnlockGestureLayout.isInitialized) {
|
|
310
|
+
supportUnlockGestureLayout.cancelHold()
|
|
311
|
+
supportUnlockGestureLayout.onUnlock = null
|
|
312
|
+
}
|
|
247
313
|
|
|
248
314
|
if (::runtime.isInitialized) {
|
|
249
315
|
|
package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsAccessManager.kt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.pakstr.app.debug
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.pakstr.app.BuildConfig
|
|
5
|
+
import com.pakstr.app.RuntimeConfig
|
|
6
|
+
|
|
7
|
+
object DeveloperToolsAccessManager {
|
|
8
|
+
|
|
9
|
+
fun canOpenDeveloperTools(context: Context): Boolean {
|
|
10
|
+
return BuildConfig.DEBUG ||
|
|
11
|
+
RuntimeConfig.debugEnabled(context) ||
|
|
12
|
+
SupportUnlock.isActive(context)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.pakstr.app.debug
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
|
|
5
|
+
object SupportUnlock {
|
|
6
|
+
|
|
7
|
+
const val UNLOCK_DURATION_MS = 60 * 60 * 1000L
|
|
8
|
+
|
|
9
|
+
private const val PREFERENCES_NAME = "pakstr_support_unlock"
|
|
10
|
+
private const val EXPIRES_AT_KEY = "expires_at"
|
|
11
|
+
|
|
12
|
+
fun unlock(context: Context) {
|
|
13
|
+
preferences(context)
|
|
14
|
+
.edit()
|
|
15
|
+
.putLong(EXPIRES_AT_KEY, System.currentTimeMillis() + UNLOCK_DURATION_MS)
|
|
16
|
+
.apply()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun isActive(context: Context): Boolean {
|
|
20
|
+
val remainingDuration = remainingDuration(context)
|
|
21
|
+
|
|
22
|
+
if (remainingDuration <= 0L) {
|
|
23
|
+
clear(context)
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun clear(context: Context) {
|
|
31
|
+
preferences(context)
|
|
32
|
+
.edit()
|
|
33
|
+
.remove(EXPIRES_AT_KEY)
|
|
34
|
+
.apply()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun remainingDuration(context: Context): Long {
|
|
38
|
+
val expiresAt = preferences(context).getLong(EXPIRES_AT_KEY, 0L)
|
|
39
|
+
return (expiresAt - System.currentTimeMillis()).coerceAtLeast(0L)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun preferences(context: Context) =
|
|
43
|
+
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
44
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package com.pakstr.app.debug
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.util.AttributeSet
|
|
7
|
+
import android.view.MotionEvent
|
|
8
|
+
import android.view.ViewConfiguration
|
|
9
|
+
import android.widget.FrameLayout
|
|
10
|
+
import kotlin.math.abs
|
|
11
|
+
|
|
12
|
+
class SupportUnlockGestureLayout @JvmOverloads constructor(
|
|
13
|
+
context: Context,
|
|
14
|
+
attrs: AttributeSet? = null,
|
|
15
|
+
defStyleAttr: Int = 0
|
|
16
|
+
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
17
|
+
|
|
18
|
+
var onUnlock: (() -> Unit)? = null
|
|
19
|
+
|
|
20
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
21
|
+
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
22
|
+
private val cornerSize = CORNER_SIZE_DP * resources.displayMetrics.density
|
|
23
|
+
|
|
24
|
+
private var tracking = false
|
|
25
|
+
private var downX = 0f
|
|
26
|
+
private var downY = 0f
|
|
27
|
+
|
|
28
|
+
private val unlockRunnable = Runnable {
|
|
29
|
+
if (tracking) {
|
|
30
|
+
tracking = false
|
|
31
|
+
onUnlock?.invoke()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
36
|
+
observeTouch(event)
|
|
37
|
+
return super.dispatchTouchEvent(event)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun cancelHold() {
|
|
41
|
+
tracking = false
|
|
42
|
+
handler.removeCallbacks(unlockRunnable)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onDetachedFromWindow() {
|
|
46
|
+
cancelHold()
|
|
47
|
+
super.onDetachedFromWindow()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private fun observeTouch(event: MotionEvent) {
|
|
51
|
+
when (event.actionMasked) {
|
|
52
|
+
MotionEvent.ACTION_DOWN -> {
|
|
53
|
+
if (isInTopRightCorner(event.x, event.y)) {
|
|
54
|
+
tracking = true
|
|
55
|
+
downX = event.x
|
|
56
|
+
downY = event.y
|
|
57
|
+
handler.postDelayed(unlockRunnable, HOLD_DURATION_MS)
|
|
58
|
+
} else {
|
|
59
|
+
cancelHold()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
MotionEvent.ACTION_MOVE -> {
|
|
64
|
+
if (tracking && hasMovedBeyondTouchSlop(event.x, event.y)) {
|
|
65
|
+
cancelHold()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
MotionEvent.ACTION_POINTER_DOWN,
|
|
70
|
+
MotionEvent.ACTION_UP,
|
|
71
|
+
MotionEvent.ACTION_CANCEL -> cancelHold()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun isInTopRightCorner(x: Float, y: Float): Boolean {
|
|
76
|
+
return x >= width - cornerSize && y <= paddingTop + cornerSize
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun hasMovedBeyondTouchSlop(x: Float, y: Float): Boolean {
|
|
80
|
+
return abs(x - downX) > touchSlop || abs(y - downY) > touchSlop
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private companion object {
|
|
84
|
+
const val HOLD_DURATION_MS = 10_000L
|
|
85
|
+
const val CORNER_SIZE_DP = 72
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<
|
|
2
|
+
<com.pakstr.app.debug.SupportUnlockGestureLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
3
|
android:id="@+id/mainContainer"
|
|
4
4
|
android:layout_width="match_parent"
|
|
5
5
|
android:layout_height="match_parent"
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
android:contentDescription="@string/developer_tools"
|
|
28
28
|
android:src="@android:drawable/ic_menu_info_details" />
|
|
29
29
|
|
|
30
|
-
</
|
|
30
|
+
</com.pakstr.app.debug.SupportUnlockGestureLayout>
|
|
@@ -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() {
|