Update snet.jar extension
The existing API key was revoked for some reason. Release an updated extension jar with a new API key. In addition, add some offline signature verification and change how results are parsed to workaround some dumbass Xposed module "faking" success results, since many users really don't know better.
This commit is contained in:
parent
c4e8dda37c
commit
94ec11db58
@ -207,7 +207,7 @@ dependencies {
|
|||||||
implementation("io.noties.markwon:image:${vMarkwon}")
|
implementation("io.noties.markwon:image:${vMarkwon}")
|
||||||
implementation("com.caverock:androidsvg:1.4")
|
implementation("com.caverock:androidsvg:1.4")
|
||||||
|
|
||||||
val vLibsu = "3.1.1"
|
val vLibsu = "3.1.2"
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ object Const {
|
|||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
// Versions
|
// Versions
|
||||||
const val SNET_EXT_VER = 15
|
const val SNET_EXT_VER = 16
|
||||||
const val SNET_REVISION = "22.0"
|
const val SNET_REVISION = "22.0"
|
||||||
const val BOOTCTL_REVISION = "22.0"
|
const val BOOTCTL_REVISION = "22.0"
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.data.repository
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||||
@ -66,7 +65,7 @@ class NetworkService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch files
|
// Fetch files
|
||||||
suspend fun fetchSafetynet() = wrap { jsd.fetchSafetynet() }
|
// suspend fun fetchSafetynet() = wrap { jsd.fetchSafetynet() }
|
||||||
suspend fun fetchBootctl() = wrap { jsd.fetchBootctl() }
|
suspend fun fetchBootctl() = wrap { jsd.fetchBootctl() }
|
||||||
suspend fun fetchInstaller() = wrap {
|
suspend fun fetchInstaller() = wrap {
|
||||||
val sha = fetchMainVersion()
|
val sha = fetchMainVersion()
|
||||||
@ -76,4 +75,7 @@ class NetworkService(
|
|||||||
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
||||||
|
|
||||||
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
|
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
|
||||||
|
|
||||||
|
// Temporary for canary builds, switch to release link at next public release
|
||||||
|
suspend fun fetchSafetynet() = fetchFile("https://github.com/topjohnwu/Magisk/releases/download/v22.1/snet.jar")
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
package com.topjohnwu.magisk.ui.safetynet
|
package com.topjohnwu.magisk.ui.safetynet
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Base64
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.ContextExecutor
|
import com.topjohnwu.magisk.arch.ContextExecutor
|
||||||
import com.topjohnwu.magisk.arch.ViewEventWithScope
|
import com.topjohnwu.magisk.arch.ViewEventWithScope
|
||||||
@ -9,21 +15,29 @@ import com.topjohnwu.magisk.data.repository.NetworkService
|
|||||||
import com.topjohnwu.magisk.ktx.DynamicClassLoader
|
import com.topjohnwu.magisk.ktx.DynamicClassLoader
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
import com.topjohnwu.signing.CryptoUtils
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import dalvik.system.BaseDexClassLoader
|
||||||
import dalvik.system.DexFile
|
import dalvik.system.DexFile
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.bouncycastle.asn1.ASN1Encoding
|
||||||
|
import org.bouncycastle.asn1.ASN1Primitive
|
||||||
|
import org.bouncycastle.est.jcajce.JsseDefaultHostnameAuthorizer
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.inject
|
import org.koin.core.inject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
class CheckSafetyNetEvent(
|
class CheckSafetyNetEvent(
|
||||||
private val callback: (SafetyNetResult) -> Unit = {}
|
private val callback: (SafetyNetResult) -> Unit = {}
|
||||||
) : ViewEventWithScope(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
|
) : ViewEventWithScope(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
|
||||||
@ -32,92 +46,94 @@ class CheckSafetyNetEvent(
|
|||||||
|
|
||||||
private lateinit var apk: File
|
private lateinit var apk: File
|
||||||
private lateinit var dex: File
|
private lateinit var dex: File
|
||||||
|
private lateinit var nonce: ByteArray
|
||||||
|
|
||||||
override fun invoke(context: Context) {
|
override fun invoke(context: Context) {
|
||||||
apk = File("${context.filesDir.parent}/snet", "snet.jar")
|
apk = File("${context.filesDir.parent}/snet", "snet.jar")
|
||||||
dex = File(apk.parent, "snet.dex")
|
dex = File(apk.parent, "snet.dex")
|
||||||
|
|
||||||
scope.launch {
|
scope.launch(Dispatchers.IO) {
|
||||||
attest(context) {
|
attest(context) {
|
||||||
// Download and retry
|
// Download and retry
|
||||||
withContext(Dispatchers.IO) {
|
Shell.sh("rm -rf " + apk.parent).exec()
|
||||||
Shell.sh("rm -rf " + apk.parent).exec()
|
apk.parentFile?.mkdir()
|
||||||
apk.parentFile?.mkdir()
|
withContext(Dispatchers.Main) {
|
||||||
|
showDialog(context)
|
||||||
}
|
}
|
||||||
download(context, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) {
|
private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) {
|
||||||
|
val helper: SafetyNetHelper
|
||||||
try {
|
try {
|
||||||
val helper = withContext(Dispatchers.IO) {
|
val loader = DynamicClassLoader(apk)
|
||||||
val loader = DynamicClassLoader(apk)
|
|
||||||
val dex = DexFile.loadDex(apk.path, dex.path, 0)
|
|
||||||
|
|
||||||
// Scan through the dex and find our helper class
|
// Scan through the dex and find our helper class
|
||||||
var helperClass: Class<*>? = null
|
var clazz: Class<*>? = null
|
||||||
for (className in dex.entries()) {
|
loop@for (dex in loader.getDexFiles()) {
|
||||||
if (className.startsWith("x.")) {
|
for (name in dex.entries()) {
|
||||||
val cls = loader.loadClass(className)
|
if (name.startsWith("x.")) {
|
||||||
|
val cls = loader.loadClass(name)
|
||||||
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
|
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
|
||||||
helperClass = cls
|
clazz = cls
|
||||||
break
|
break@loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
helperClass ?: throw Exception()
|
|
||||||
|
|
||||||
val helper = helperClass
|
|
||||||
.getMethod("get", Class::class.java, Context::class.java, Any::class.java)
|
|
||||||
.invoke(
|
|
||||||
null, SafetyNetHelper::class.java,
|
|
||||||
context, this@CheckSafetyNetEvent
|
|
||||||
) as SafetyNetHelper
|
|
||||||
|
|
||||||
if (helper.version < Const.SNET_EXT_VER)
|
|
||||||
throw Exception()
|
|
||||||
helper
|
|
||||||
}
|
}
|
||||||
helper.attest()
|
clazz ?: throw Exception("Cannot find SafetyNetHelper class")
|
||||||
|
|
||||||
|
helper = clazz.getMethod("get", Class::class.java, Context::class.java, Any::class.java)
|
||||||
|
.invoke(null, SafetyNetHelper::class.java, context, this) as SafetyNetHelper
|
||||||
|
|
||||||
|
if (helper.version != Const.SNET_EXT_VER)
|
||||||
|
throw Exception("snet extension version mismatch")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException)
|
|
||||||
throw e
|
|
||||||
onError(e)
|
onError(e)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
|
||||||
private fun download(context: Context, askUser: Boolean) {
|
|
||||||
fun downloadInternal() = scope.launch {
|
|
||||||
val abort: suspend (Exception) -> Unit = {
|
|
||||||
Timber.e(it)
|
|
||||||
callback(SafetyNetResult())
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
svc.fetchSafetynet().byteStream().writeTo(apk)
|
|
||||||
}
|
|
||||||
attest(context, abort)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (e is CancellationException)
|
|
||||||
throw e
|
|
||||||
abort(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!askUser) {
|
|
||||||
downloadInternal()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val random = SecureRandom()
|
||||||
|
nonce = ByteArray(24)
|
||||||
|
random.nextBytes(nonce)
|
||||||
|
helper.attest(nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Class<*>.field(name: String): Field =
|
||||||
|
getDeclaredField(name).apply { isAccessible = true }
|
||||||
|
|
||||||
|
// All of these fields are whitelisted
|
||||||
|
private fun BaseDexClassLoader.getDexFiles(): List<DexFile> {
|
||||||
|
val pathList = BaseDexClassLoader::class.java.field("pathList").get(this)
|
||||||
|
val dexElements = pathList.javaClass.field("dexElements").get(pathList) as Array<*>
|
||||||
|
val fileField = dexElements.javaClass.componentType.field("dexFile")
|
||||||
|
return dexElements.map { fileField.get(it) as DexFile }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun download(context: Context) = scope.launch(Dispatchers.IO) {
|
||||||
|
val abort: suspend (Exception) -> Unit = {
|
||||||
|
Timber.e(it)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
callback(SafetyNetResult())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
svc.fetchSafetynet().byteStream().writeTo(apk)
|
||||||
|
attest(context, abort)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
abort(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDialog(context: Context) {
|
||||||
MagiskDialog(context)
|
MagiskDialog(context)
|
||||||
.applyTitle(R.string.proprietary_title)
|
.applyTitle(R.string.proprietary_title)
|
||||||
.applyMessage(R.string.proprietary_notice)
|
.applyMessage(R.string.proprietary_notice)
|
||||||
.cancellable(false)
|
.cancellable(false)
|
||||||
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
titleRes = android.R.string.ok
|
titleRes = android.R.string.ok
|
||||||
onClick { downloadInternal() }
|
onClick { download(context) }
|
||||||
}
|
}
|
||||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
titleRes = android.R.string.cancel
|
titleRes = android.R.string.cancel
|
||||||
@ -129,7 +145,94 @@ class CheckSafetyNetEvent(
|
|||||||
.reveal()
|
.reveal()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(response: JSONObject?) {
|
private fun String.decode(): ByteArray {
|
||||||
callback(SafetyNetResult(response))
|
return if (contains("\\+|/".toRegex()))
|
||||||
|
Base64.decode(this, Base64.DEFAULT)
|
||||||
|
else
|
||||||
|
Base64.decode(this, Base64.URL_SAFE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.parseJws(): SafetyNetResponse? {
|
||||||
|
val jws = split('.')
|
||||||
|
val secondDot = lastIndexOf('.')
|
||||||
|
val rawHeader = String(jws[0].decode())
|
||||||
|
val payload = String(jws[1].decode())
|
||||||
|
var signature = jws[2].decode()
|
||||||
|
val signedBytes = substring(0, secondDot).toByteArray()
|
||||||
|
|
||||||
|
val moshi = Moshi.Builder().build()
|
||||||
|
val header = moshi.adapter(JwsHeader::class.java).fromJson(rawHeader) ?: return null
|
||||||
|
|
||||||
|
val alg = when (header.algorithm) {
|
||||||
|
"RS256" -> "SHA256withRSA"
|
||||||
|
"ES256" -> {
|
||||||
|
// Convert to DER encoding
|
||||||
|
signature = ASN1Primitive.fromByteArray(signature).getEncoded(ASN1Encoding.DER)
|
||||||
|
"SHA256withECDSA"
|
||||||
|
}
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
val certB64 = header.certificates?.first() ?: return null
|
||||||
|
val certDer = certB64.decode()
|
||||||
|
val bis = ByteArrayInputStream(certDer)
|
||||||
|
val cert: X509Certificate
|
||||||
|
try {
|
||||||
|
cert = CryptoUtils.readCertificate(bis)
|
||||||
|
val verifier = Signature.getInstance(alg)
|
||||||
|
verifier.initVerify(cert.publicKey)
|
||||||
|
verifier.update(signedBytes)
|
||||||
|
if (!verifier.verify(signature))
|
||||||
|
return null
|
||||||
|
} catch (e: GeneralSecurityException) {
|
||||||
|
Timber.e(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hostname
|
||||||
|
val hostNameVerifier = JsseDefaultHostnameAuthorizer(setOf())
|
||||||
|
try {
|
||||||
|
if (!hostNameVerifier.verify("attest.android.com", cert))
|
||||||
|
return null
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = moshi.adapter(SafetyNetResponse::class.java).fromJson(payload) ?: return null
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if (!response.nonce.decode().contentEquals(nonce))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(response: String?) {
|
||||||
|
if (response != null) {
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
val res = response.parseJws()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
callback(SafetyNetResult(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(SafetyNetResult())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class JwsHeader(
|
||||||
|
@Json(name = "alg") val algorithm: String,
|
||||||
|
@Json(name = "x5c") val certificates: List<String>?
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class SafetyNetResponse(
|
||||||
|
val nonce: String,
|
||||||
|
val ctsProfileMatch: Boolean,
|
||||||
|
val basicIntegrity: Boolean,
|
||||||
|
val evaluationType: String = ""
|
||||||
|
)
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
package com.topjohnwu.magisk.ui.safetynet
|
package com.topjohnwu.magisk.ui.safetynet
|
||||||
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
interface SafetyNetHelper {
|
interface SafetyNetHelper {
|
||||||
|
|
||||||
val version: Int
|
val version: Int
|
||||||
|
|
||||||
fun attest()
|
fun attest(nonce: ByteArray)
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onResponse(response: JSONObject?)
|
fun onResponse(response: String?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,9 @@ import com.topjohnwu.magisk.BR
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
data class SafetyNetResult(
|
data class SafetyNetResult(
|
||||||
val response: JSONObject? = null,
|
val response: SafetyNetResponse? = null,
|
||||||
val dismiss: Boolean = false
|
val dismiss: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,20 +42,20 @@ class SafetynetViewModel : BaseViewModel() {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
cachedResult?.also {
|
cachedResult?.also {
|
||||||
resolveResponse(SafetyNetResult(it))
|
handleResponse(SafetyNetResult(it))
|
||||||
} ?: attest()
|
} ?: attest()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attest() {
|
private fun attest() {
|
||||||
isChecking = true
|
isChecking = true
|
||||||
CheckSafetyNetEvent {
|
CheckSafetyNetEvent {
|
||||||
resolveResponse(it)
|
handleResponse(it)
|
||||||
}.publish()
|
}.publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() = attest()
|
fun reset() = attest()
|
||||||
|
|
||||||
private fun resolveResponse(response: SafetyNetResult) {
|
private fun handleResponse(response: SafetyNetResult) {
|
||||||
isChecking = false
|
isChecking = false
|
||||||
|
|
||||||
if (response.dismiss) {
|
if (response.dismiss) {
|
||||||
@ -65,26 +64,15 @@ class SafetynetViewModel : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response.response?.apply {
|
response.response?.apply {
|
||||||
runCatching {
|
val result = ctsProfileMatch && basicIntegrity
|
||||||
val cts = getBoolean("ctsProfileMatch")
|
cachedResult = this
|
||||||
val basic = getBoolean("basicIntegrity")
|
ctsState = ctsProfileMatch
|
||||||
val eval = optString("evaluationType")
|
basicIntegrityState = basicIntegrity
|
||||||
val result = cts && basic
|
evalType = if (evaluationType.contains("HARDWARE")) "HARDWARE" else "BASIC"
|
||||||
cachedResult = this
|
isSuccess = result
|
||||||
ctsState = cts
|
safetyNetTitle =
|
||||||
basicIntegrityState = basic
|
if (result) R.string.safetynet_attest_success
|
||||||
evalType = if (eval.contains("HARDWARE")) "HARDWARE" else "BASIC"
|
else R.string.safetynet_attest_failure
|
||||||
isSuccess = result
|
|
||||||
safetyNetTitle =
|
|
||||||
if (result) R.string.safetynet_attest_success
|
|
||||||
else R.string.safetynet_attest_failure
|
|
||||||
}.onFailure {
|
|
||||||
isSuccess = false
|
|
||||||
ctsState = false
|
|
||||||
basicIntegrityState = false
|
|
||||||
evalType = "N/A"
|
|
||||||
safetyNetTitle = R.string.safetynet_res_invalid
|
|
||||||
}
|
|
||||||
} ?: {
|
} ?: {
|
||||||
isSuccess = false
|
isSuccess = false
|
||||||
ctsState = false
|
ctsState = false
|
||||||
@ -95,7 +83,7 @@ class SafetynetViewModel : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var cachedResult: JSONObject? = null
|
private var cachedResult: SafetyNetResponse? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
2
build.py
2
build.py
@ -363,7 +363,7 @@ def build_stub(args):
|
|||||||
|
|
||||||
|
|
||||||
def build_snet(args):
|
def build_snet(args):
|
||||||
if not op.exists(op.join('snet', 'src', 'main', 'java', 'com', 'topjohnwu', 'snet')):
|
if not op.exists(op.join('stub', 'src', 'main', 'java', 'com', 'topjohnwu', 'snet')):
|
||||||
error('snet sources have to be bind mounted on top of the stub folder')
|
error('snet sources have to be bind mounted on top of the stub folder')
|
||||||
header('* Building snet extension')
|
header('* Building snet extension')
|
||||||
proc = execv([gradlew, 'stub:assembleRelease'])
|
proc = execv([gradlew, 'stub:assembleRelease'])
|
||||||
|
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,6 @@
|
|||||||
|
#Wed Apr 14 22:32:11 PDT 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
|
||||||
|
Loading…
Reference in New Issue
Block a user