chore: initial import
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
[versions]
|
||||
agp = "8.9.1"
|
||||
kotlin = "2.1.20"
|
||||
composeBom = "2025.05.01"
|
||||
activityCompose = "1.10.1"
|
||||
lifecycle = "2.9.0"
|
||||
navigation = "2.9.0"
|
||||
hilt = "2.56.2"
|
||||
hiltNavigation = "1.2.0"
|
||||
room = "2.7.1"
|
||||
datastore = "1.1.7"
|
||||
work = "2.10.1"
|
||||
retrofit = "2.11.0"
|
||||
okhttp = "4.12.0"
|
||||
serialization = "1.8.1"
|
||||
coreKtx = "1.16.0"
|
||||
securityCrypto = "1.1.0-alpha06"
|
||||
junit = "4.13.2"
|
||||
androidxJunit = "1.2.1"
|
||||
espresso = "3.6.1"
|
||||
testRunner = "1.6.2"
|
||||
testCore = "1.6.1"
|
||||
mockwebserver = "4.12.0"
|
||||
coroutines = "1.10.2"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
|
||||
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||
compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
compose-icons = { module = "androidx.compose.material:material-icons-extended" }
|
||||
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
|
||||
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
||||
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigation" }
|
||||
hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltNavigation" }
|
||||
hilt-work-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltNavigation" }
|
||||
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
|
||||
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
|
||||
work-testing = { module = "androidx.work:work-testing", version.ref = "work" }
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
retrofit-kotlinx = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxJunit" }
|
||||
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
||||
androidx-test-runner = { module = "androidx.test:runner", version.ref = "testRunner" }
|
||||
androidx-test-core-ktx = { module = "androidx.test:core-ktx", version.ref = "testCore" }
|
||||
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
|
||||
mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package org.gradle.wrapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* Small auditable Gradle wrapper bootstrap used because the official wrapper JAR
|
||||
* could not be generated in the build container. It downloads the configured
|
||||
* distribution, validates SHA-256, extracts it with zip-slip protection and
|
||||
* starts Gradle in the project directory.
|
||||
*/
|
||||
public final class GradleWrapperMain {
|
||||
private GradleWrapperMain() {}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Path jar = Paths.get(GradleWrapperMain.class.getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||
Path projectRoot = jar.getParent().getParent().getParent();
|
||||
Path propertiesPath = jar.resolveSibling("gradle-wrapper.properties");
|
||||
Properties properties = new Properties();
|
||||
try (InputStream input = Files.newInputStream(propertiesPath)) {
|
||||
properties.load(input);
|
||||
}
|
||||
|
||||
URI distributionUri = URI.create(required(properties, "distributionUrl"));
|
||||
String archiveName = Paths.get(distributionUri.getPath()).getFileName().toString();
|
||||
String distributionName = archiveName.replaceFirst("-(bin|all)\\.zip$", "");
|
||||
Path distributionDirectory = gradleUserHome(args)
|
||||
.resolve(properties.getProperty("distributionPath", "wrapper/dists"))
|
||||
.resolve(distributionName)
|
||||
.resolve(archiveName);
|
||||
Path archive = distributionDirectory.resolve(archiveName);
|
||||
Path marker = distributionDirectory.resolve(".installed");
|
||||
Files.createDirectories(distributionDirectory);
|
||||
|
||||
if (!Files.exists(marker)) {
|
||||
if (!Files.exists(archive)) {
|
||||
download(distributionUri.toURL(), archive, intProperty(properties, "networkTimeout", 10_000));
|
||||
}
|
||||
String expectedSha = properties.getProperty("distributionSha256Sum", "").trim();
|
||||
if (!expectedSha.isEmpty()) {
|
||||
verifySha256(archive, expectedSha);
|
||||
}
|
||||
unzip(archive, distributionDirectory);
|
||||
Files.writeString(marker, "ok\n", StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
Path gradleHome;
|
||||
try (Stream<Path> children = Files.list(distributionDirectory)) {
|
||||
gradleHome = children
|
||||
.filter(Files::isDirectory)
|
||||
.filter(path -> path.getFileName().toString().startsWith("gradle-"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IOException("Extracted Gradle directory not found in " + distributionDirectory));
|
||||
}
|
||||
|
||||
boolean windows = System.getProperty("os.name", "").toLowerCase().contains("win");
|
||||
Path executable = gradleHome.resolve("bin").resolve(windows ? "gradle.bat" : "gradle");
|
||||
if (!windows) executable.toFile().setExecutable(true);
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(executable.toString());
|
||||
for (String argument : args) command.add(argument);
|
||||
Process process = new ProcessBuilder(command)
|
||||
.directory(projectRoot.toFile())
|
||||
.inheritIO()
|
||||
.start();
|
||||
System.exit(process.waitFor());
|
||||
}
|
||||
|
||||
private static Path gradleUserHome(String[] args) {
|
||||
for (int index = 0; index < args.length - 1; index++) {
|
||||
if ("-g".equals(args[index]) || "--gradle-user-home".equals(args[index])) {
|
||||
return Paths.get(args[index + 1]).toAbsolutePath();
|
||||
}
|
||||
}
|
||||
String configured = System.getenv("GRADLE_USER_HOME");
|
||||
return configured == null || configured.isBlank()
|
||||
? Paths.get(System.getProperty("user.home"), ".gradle")
|
||||
: Paths.get(configured).toAbsolutePath();
|
||||
}
|
||||
|
||||
private static void download(URL initialUrl, Path destination, int timeoutMillis) throws Exception {
|
||||
URL url = initialUrl;
|
||||
for (int redirect = 0; redirect < 8; redirect++) {
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setConnectTimeout(timeoutMillis);
|
||||
connection.setReadTimeout(Math.max(timeoutMillis, 30_000));
|
||||
connection.setRequestProperty("User-Agent", "boehmitools-training-gradle-wrapper");
|
||||
if (connection instanceof HttpURLConnection http) {
|
||||
http.setInstanceFollowRedirects(false);
|
||||
int status = http.getResponseCode();
|
||||
if (status >= 300 && status < 400) {
|
||||
String location = http.getHeaderField("Location");
|
||||
if (location == null) throw new IOException("Redirect without Location from " + url);
|
||||
url = url.toURI().resolve(location).toURL();
|
||||
http.disconnect();
|
||||
continue;
|
||||
}
|
||||
if (status >= 400) throw new IOException("HTTP " + status + " while downloading " + url);
|
||||
}
|
||||
Path partial = destination.resolveSibling(destination.getFileName() + ".part");
|
||||
try (InputStream input = connection.getInputStream(); OutputStream output = Files.newOutputStream(partial)) {
|
||||
input.transferTo(output);
|
||||
}
|
||||
Files.move(partial, destination, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
return;
|
||||
}
|
||||
throw new IOException("Too many redirects while downloading " + initialUrl);
|
||||
}
|
||||
|
||||
private static void verifySha256(Path file, String expected) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
for (int read; (read = input.read(buffer)) >= 0;) {
|
||||
if (read > 0) digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
String actual = HexFormat.of().formatHex(digest.digest());
|
||||
if (!actual.equalsIgnoreCase(expected)) {
|
||||
Files.deleteIfExists(file);
|
||||
throw new IOException("Gradle distribution SHA-256 mismatch: expected " + expected + ", got " + actual);
|
||||
}
|
||||
}
|
||||
|
||||
private static void unzip(Path archive, Path destination) throws IOException {
|
||||
try (ZipInputStream zip = new ZipInputStream(Files.newInputStream(archive))) {
|
||||
for (ZipEntry entry; (entry = zip.getNextEntry()) != null;) {
|
||||
Path target = destination.resolve(entry.getName()).normalize();
|
||||
if (!target.startsWith(destination.normalize())) {
|
||||
throw new IOException("Unsafe ZIP entry: " + entry.getName());
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(target);
|
||||
} else {
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.copy(zip, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
zip.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String required(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) throw new IllegalArgumentException("Missing " + key);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int intProperty(Properties properties, String key, int fallback) {
|
||||
try {
|
||||
return Integer.parseInt(properties.getProperty(key, Integer.toString(fallback)));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
|
||||
Reference in New Issue
Block a user