From 7b00cff70e3aae48c5ecdd17bed99159293969bf Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 13 Jul 2026 02:21:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20Gradle=20Wrapper=20Neo?= =?UTF-8?q?=20=E4=BB=A3=E6=9B=BF=20Gradle=20Wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/GradleWrapperNeo.java | 4105 ++++++++++++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 45457 -> 0 bytes gradlew | 125 +- gradlew.bat | 83 +- gradlew.ps1 | 373 +++ 5 files changed, 4611 insertions(+), 75 deletions(-) create mode 100644 gradle/wrapper/GradleWrapperNeo.java delete mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradlew.ps1 diff --git a/gradle/wrapper/GradleWrapperNeo.java b/gradle/wrapper/GradleWrapperNeo.java new file mode 100644 index 00000000000..2a166c3107f --- /dev/null +++ b/gradle/wrapper/GradleWrapperNeo.java @@ -0,0 +1,4105 @@ +/* + * Gradle Wrapper Neo single-file source distribution. + * + * This source file is part of Gradle Wrapper Neo 0.1.0. + * Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo + * + * Place this file at gradle/wrapper/GradleWrapperNeo.java in a Gradle project. + * Keep gradle-wrapper.properties in the same directory and the Gradle Wrapper Neo + * launch scripts (gradlew, gradlew.bat, and gradlew.ps1) in the project root. + * Run ./gradlew on POSIX systems or gradlew.bat on Windows as you would use the + * standard Gradle Wrapper. + */ + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.Authenticator; +import java.net.HttpURLConnection; +import java.net.PasswordAuthentication; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.function.Function; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; +import javax.lang.model.SourceVersion; +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import static java.lang.String.format; +import static java.util.Collections.emptyList; + +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +public class GradleWrapperNeo { + + public static final String GRADLE_USER_HOME_OPTION = "g"; + + public static final String GRADLE_USER_HOME_DETAILED_OPTION = "gradle-user-home"; + + public static final String GRADLE_QUIET_OPTION = "q"; + + public static final String GRADLE_QUIET_DETAILED_OPTION = "quiet"; + + public static void main(String[] args) throws Exception { + if (Bootstrap.handle(args, GradleWrapperNeo.class)) { + return; + } + prepareWrapper(args).execute(); + } + + private static Action prepareWrapper(String[] args) throws Exception { + File appHome = appHome(); + File propertiesFile = wrapperProperties(); + CommandLineParser parser = new CommandLineParser(); + parser.allowUnknownOptions(); + parser.option(GRADLE_USER_HOME_OPTION, GRADLE_USER_HOME_DETAILED_OPTION).hasArgument(); + parser.option(GRADLE_QUIET_OPTION, GRADLE_QUIET_DETAILED_OPTION); + SystemPropertiesCommandLineConverter converter = new SystemPropertiesCommandLineConverter(); + converter.configure(parser); + ParsedCommandLine options = parser.parse(args); + Map commandLineSystemProperties = converter.convert(options, new HashMap()); + Map projectSystemProperties = PropertiesFileHandler.getSystemProperties(new File(appHome, "gradle.properties")); + // If the Gradle system properties may define a custom Gradle home, which needs to be set before loading user gradle.properties + maybeAddGradleUserHomeSystemProperty(projectSystemProperties, commandLineSystemProperties); + File gradleUserHome = gradleUserHome(options); + File userGradleProperties = new File(gradleUserHome, "gradle.properties"); + Map userSystemProperties = new HashMap<>(PropertiesFileHandler.getSystemProperties(userGradleProperties)); + // Inception: Gradle user home cannot be changed with configuration from the Gradle user home + boolean invalidGradleUserHome = userSystemProperties.remove(GradleUserHomeLookup.GRADLE_USER_HOME_PROPERTY_KEY) != null; + // Set all system properties from all Gradle sources with correct precedence: project (lowest) < user < cli (highest) + addSystemProperties(projectSystemProperties, userSystemProperties, commandLineSystemProperties); + Logger logger = logger(options); + if (invalidGradleUserHome) { + logger.log("WARNING Ignored custom Gradle user home location configured in Gradle user home: " + userGradleProperties.getAbsolutePath()); + } + WrapperExecutor wrapperExecutor = WrapperExecutor.forWrapperPropertiesFile(propertiesFile); + WrapperConfiguration configuration = wrapperExecutor.getConfiguration(); + configuration.setMirrorConfiguration(MirrorConfiguration.load(gradleUserHome)); + IDownload download = new Download(logger, "gradlew", Download.UNKNOWN_VERSION, configuration.getNetworkTimeout()); + return () -> { + wrapperExecutor.execute(args, new Install(logger, download, new PathAssembler(gradleUserHome, appHome)), new BootstrapMainStarter()); + }; + } + + private static void addSystemProperties(Map projectSystemProperties, Map userSystemProperties, Map commandLineSystemProperties) { + Map gradleSystemProperties = merge(merge(projectSystemProperties, userSystemProperties), commandLineSystemProperties); + System.getProperties().putAll(gradleSystemProperties); + } + + private static void maybeAddGradleUserHomeSystemProperty(Map projectSystemProperties, Map commandLineSystemProperties) { + Map gradleSystemProperties = merge(projectSystemProperties, commandLineSystemProperties); + String property = gradleSystemProperties.get(GradleUserHomeLookup.GRADLE_USER_HOME_PROPERTY_KEY); + if (property != null) { + System.setProperty(GradleUserHomeLookup.GRADLE_USER_HOME_PROPERTY_KEY, property); + } + } + + private static Map merge(Map p1, Map p2) { + // If there are duplicate keys, the values from p2 take precedence. + Map result = new HashMap<>(p1); + result.putAll(p2); + return result; + } + + static File appHome() { + return Bootstrap.appHome().toFile(); + } + + static File wrapperProperties() { + return WrapperExecutor.wrapperPropertiesForProjectDirectory(appHome()); + } + + private static File gradleUserHome(ParsedCommandLine options) { + if (options.hasOption(GRADLE_USER_HOME_OPTION)) { + return new File(options.option(GRADLE_USER_HOME_OPTION).getValue()); + } + return GradleUserHomeLookup.gradleUserHome(); + } + + private static Logger logger(ParsedCommandLine options) { + return new Logger(options.hasOption(GRADLE_QUIET_OPTION)); + } + + // @NullMarked + @FunctionalInterface + private interface Action { + + void execute() throws Exception; + } +} + +/* + * Copyright 2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +abstract class AbstractCommandLineConverter implements CommandLineConverter { + + @Override + public T convert(Iterable args, T target) throws CommandLineArgumentException { + CommandLineParser parser = new CommandLineParser(); + configure(parser); + return convert(parser.parse(args), target); + } +} + +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +abstract class AbstractPropertiesCommandLineConverter extends AbstractCommandLineConverter> { + + protected abstract String getPropertyOption(); + + protected abstract String getPropertyOptionDetailed(); + + protected abstract String getPropertyOptionDescription(); + + protected abstract OptionCategory getCategory(); + + @Override + public void configure(CommandLineParser parser) { + CommandLineOption option = parser.option(getPropertyOption(), getPropertyOptionDetailed()); + option = option.hasArguments(); + option.hasDescription(getPropertyOptionDescription()); + option.hasCategory(getCategory()); + } + + @Override + public Map convert(ParsedCommandLine options, Map properties) throws CommandLineArgumentException { + for (String keyValueExpression : options.option(getPropertyOption()).getValues()) { + int pos = keyValueExpression.indexOf("="); + if (pos < 0) { + properties.put(keyValueExpression, ""); + } else { + properties.put(keyValueExpression.substring(0, pos), keyValueExpression.substring(pos + 1)); + } + } + return properties; + } +} + +/* + * Copyright 2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +/** + * A {@code CommandLineArgumentException} is thrown when command-line arguments cannot be parsed. + */ +class CommandLineArgumentException extends RuntimeException { + + public CommandLineArgumentException(String message) { + super(message); + } + + public CommandLineArgumentException(String message, Throwable cause) { + super(message, cause); + } +} + +/* + * Copyright 2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +interface CommandLineConverter { + + T convert(Iterable args, T target) throws CommandLineArgumentException; + + T convert(ParsedCommandLine args, T target) throws CommandLineArgumentException; + + void configure(CommandLineParser parser); +} + +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +class CommandLineOption { + + private final Set options = new HashSet(); + + private Class argumentType = Void.TYPE; + + private String /* // @Nullable */ description; + + private boolean incubating; + + private final Set groupWith = new HashSet(); + + private boolean deprecated; + + private OptionCategory category = OptionCategory.OTHER; + + public CommandLineOption(Iterable options) { + for (String option : options) { + this.options.add(option); + } + } + + public Set getOptions() { + return options; + } + + public CommandLineOption hasArgument(Class argumentType) { + this.argumentType = argumentType; + return this; + } + + public CommandLineOption hasArgument() { + this.argumentType = String.class; + return this; + } + + public CommandLineOption hasArguments() { + argumentType = List.class; + return this; + } + + public String getDescription() { + StringBuilder result = new StringBuilder(); + if (description != null) { + result.append(description); + } + appendMessage(result, deprecated, "[deprecated]"); + appendMessage(result, incubating, "[incubating]"); + return result.toString(); + } + + private void appendMessage(StringBuilder result, boolean append, String message) { + if (append) { + if (result.length() > 0) { + result.append(' '); + } + result.append(message); + } + } + + public CommandLineOption hasDescription(String description) { + this.description = description; + return this; + } + + public boolean getAllowsArguments() { + return argumentType != Void.TYPE; + } + + public boolean getAllowsMultipleArguments() { + return argumentType == List.class; + } + + public CommandLineOption deprecated() { + this.deprecated = true; + return this; + } + + public CommandLineOption incubating() { + incubating = true; + return this; + } + + public boolean isDeprecated() { + return deprecated; + } + + public boolean isIncubating() { + return incubating; + } + + Set getGroupWith() { + return groupWith; + } + + void groupWith(Set options) { + this.groupWith.addAll(options); + this.groupWith.remove(this); + } + + public CommandLineOption hasCategory(OptionCategory category) { + this.category = category == null ? OptionCategory.OTHER : category; + return this; + } + + public OptionCategory getCategory() { + return category == null ? OptionCategory.OTHER : category; + } +} + +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +/** + *

A command-line parser which supports a command/sub-command style command-line interface. Supports the following + * syntax:

+ *
+ * <option>* (<sub-command> <sub-command-option>*)*
+ * 
+ * + *
  • Short options are a '-' followed by a single character. For example: {@code -a}.
  • + * + *
  • Long options are '--' followed by multiple characters. For example: {@code --long-option}.
  • + * + *
  • Options can take arguments. The argument follows the option. For example: {@code -a arg} or {@code --long + * arg}.
  • + * + *
  • Arguments can be attached to the option using '='. For example: {@code -a=arg} or {@code --long=arg}.
  • + * + *
  • Arguments can be attached to short options. For example: {@code -aarg}.
  • + * + *
  • Short options can be combined. For example {@code -ab} is equivalent to {@code -a -b}.
  • + * + *
  • Anything else is treated as an extra argument. This includes a single {@code -} character.
  • + * + *
  • '--' indicates the end of the options. Anything following is not parsed and is treated as extra arguments.
  • + * + *
  • The parser is forgiving, and allows '--' to be used with short options and '-' to be used with long + * options.
  • + * + *
  • The set of options must be known at parse time. Sub-commands and their options do not need to be known at parse + * time. Use {@link ParsedCommandLine#getExtraArguments()} to obtain the non-option command-line arguments.
  • + * + *
+ */ +class CommandLineParser { + + private static final Pattern OPTION_NAME_PATTERN = Pattern.compile("(\\?|\\p{Alnum}[\\p{Alnum}-_]*)"); + + private static final String DISABLE_OPTION_PREFIX = "no-"; + + private Map optionsByString = new HashMap(); + + private boolean allowMixedOptions; + + private boolean allowUnknownOptions; + + /** + * Parses the given command-line. + * + * @param commandLine The command-line. + * @return The parsed command line. + * @throws org.gradle.cli.CommandLineArgumentException + * On parse failure. + */ + public ParsedCommandLine parse(String... commandLine) throws CommandLineArgumentException { + return parse(Arrays.asList(commandLine)); + } + + /** + * Parses the given command-line. + * + * @param commandLine The command-line. + * @return The parsed command line. + * @throws org.gradle.cli.CommandLineArgumentException + * On parse failure. + */ + public ParsedCommandLine parse(Iterable commandLine) throws CommandLineArgumentException { + ParsedCommandLine parsedCommandLine = new ParsedCommandLine(new HashSet(optionsByString.values())); + ParserState parseState = new BeforeFirstSubCommand(parsedCommandLine); + for (String arg : commandLine) { + if (parseState.maybeStartOption(arg)) { + if (arg.equals("--")) { + parseState = new AfterOptions(parsedCommandLine); + } else if (arg.matches("--[^=]+")) { + OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2)); + parseState = parsedOption.onStartNextArg(); + } else if (arg.matches("(?s)--[^=]+=.*")) { + int endArg = arg.indexOf('='); + OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2, endArg)); + parseState = parsedOption.onArgument(arg.substring(endArg + 1)); + } else if (arg.matches("(?s)-[^=]=.*")) { + OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(1, 2)); + parseState = parsedOption.onArgument(arg.substring(3)); + } else { + assert arg.matches("(?s)-[^-].*"); + String option = arg.substring(1); + if (optionsByString.containsKey(option)) { + OptionParserState parsedOption = parseState.onStartOption(arg, option); + parseState = parsedOption.onStartNextArg(); + } else { + String option1 = arg.substring(1, 2); + OptionParserState parsedOption; + if (optionsByString.containsKey(option1)) { + parsedOption = parseState.onStartOption("-" + option1, option1); + if (parsedOption.getHasArgument()) { + parseState = parsedOption.onArgument(arg.substring(2)); + } else { + parseState = parsedOption.onComplete(); + for (int i = 2; i < arg.length(); i++) { + String optionStr = arg.substring(i, i + 1); + parsedOption = parseState.onStartOption("-" + optionStr, optionStr); + parseState = parsedOption.onComplete(); + } + } + } else { + if (allowUnknownOptions) { + // if we are allowing unknowns, just pass through the whole arg + parsedOption = parseState.onStartOption(arg, option); + parseState = parsedOption.onComplete(); + } else { + // We are going to throw a CommandLineArgumentException below, but want the message + // to reflect that we didn't recognise the first char (i.e. the option specifier) + parsedOption = parseState.onStartOption("-" + option1, option1); + parseState = parsedOption.onComplete(); + } + } + } + } + } else { + parseState = parseState.onNonOption(arg); + } + } + parseState.onCommandLineEnd(); + return parsedCommandLine; + } + + public CommandLineParser allowMixedSubcommandsAndOptions() { + allowMixedOptions = true; + return this; + } + + public CommandLineParser allowUnknownOptions() { + allowUnknownOptions = true; + return this; + } + + /** + * Specifies that the given set of options are mutually-exclusive. Only one of the given options will be selected. + * The parser ignores all but the last of these options. + */ + public CommandLineParser allowOneOf(String... options) { + Set commandLineOptions = new HashSet(); + for (String option : options) { + commandLineOptions.add(optionsByString.get(option)); + } + for (CommandLineOption commandLineOption : commandLineOptions) { + commandLineOption.groupWith(commandLineOptions); + } + return this; + } + + /** + * Prints a usage message to the given stream. + * + * @param out The output stream to write to. + */ + @SuppressWarnings("NullAway") + public void printUsage(Appendable out, int widthHint) { + // sort options before grouping + Set commandLineOptions = new TreeSet<>(new OptionComparator()); + commandLineOptions.addAll(optionsByString.values()); + // map options to their categories + Map> categoryToOptions = new EnumMap<>(OptionCategory.class); + for (OptionCategory category : OptionCategory.values()) { + categoryToOptions.put(category, new ArrayList<>()); + } + for (CommandLineOption option : commandLineOptions) { + categoryToOptions.get(option.getCategory()).add(RenderedCommandLineOption.from(option)); + } + // calculate column widths for option name and description + int nameColumnWidth = categoryToOptions.values().stream().flatMap(List::stream).mapToInt(o -> o.getName().length()).max().orElse(0) + // account for two extra spaces before each option plus an extra space between the longest option and its description + 3; + int descriptionColumnWidth = Math.max(30, widthHint - nameColumnWidth); + // print hint about end signal + Formatter formatter = new Formatter(out); + printRenderedOption(formatter, "--", nameColumnWidth, "Signals the end of built-in options. Parses subsequent parameters as tasks or task options only.", descriptionColumnWidth); + // print each category and its options + for (OptionCategory category : OptionCategory.values()) { + List options = categoryToOptions.get(category); + if (options == null || options.isEmpty()) { + continue; + } + // print category name + String categoryName = category.getDisplayName(); + if (!categoryName.isEmpty()) { + printCategory(formatter, categoryName); + } + // print option name and description + for (RenderedCommandLineOption option : options) { + String name = option.getName(); + String description = option.getDescription(); + printRenderedOption(formatter, " " + name, nameColumnWidth, description, descriptionColumnWidth); + } + } + formatter.flush(); + } + + private static void printRenderedOption(Formatter formatter, String name, int nameColumnWidth, String description, int descriptionColumnWidth) { + if (description == null || description.isEmpty()) { + printOption(formatter, name); + } else { + // handle multi-line descriptions and split lines that are too long for the console + List descriptionLines = Arrays.stream(description.split("\\r?\\n")).flatMap(n -> splitToLength(n, descriptionColumnWidth).stream()).collect(Collectors.toList()); + for (int i = 0; i < descriptionLines.size(); i++) { + printOption(formatter, i == 0 ? name : "", nameColumnWidth, descriptionLines.get(i)); + } + } + } + + public static List splitToLength(String input, int n) { + List lines = new ArrayList<>(); + int start = 0; + while (start < input.length()) { + int end = Math.min(start + n, input.length()); + if (end < input.length()) { + int lastSpace = input.lastIndexOf(' ', end); + if (lastSpace > start) { + end = lastSpace; + } + } + lines.add(input.substring(start, end)); + start = end; + // skip whitespace at beginning of next line + while (start < input.length() && Character.isWhitespace(input.charAt(start))) { + start++; + } + } + return lines; + } + + private static void printCategory(Formatter formatter, String name) { + formatter.format("%n%s:%n", name); + } + + private static void printOption(Formatter formatter, String name) { + formatter.format("%s%n", name); + } + + private static void printOption(Formatter formatter, String name, int nameColumnWidth, String description) { + formatter.format("%-" + nameColumnWidth + "s %s%n", name, description); + } + + // @NullMarked + private static class RenderedCommandLineOption { + + private final String name; + + private final String description; + + private RenderedCommandLineOption(String name, String description) { + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + static RenderedCommandLineOption from(CommandLineOption option) { + return new RenderedCommandLineOption(optionName(option), option.getDescription()); + } + + private static String optionName(CommandLineOption option) { + Set optionStrings = new TreeSet<>(new OptionStringComparator()); + optionStrings.addAll(option.getOptions()); + List prefixedStrings = new ArrayList<>(); + for (String optionString : optionStrings) { + if (optionString.length() == 1) { + prefixedStrings.add("-" + optionString); + } else { + prefixedStrings.add("--" + optionString); + } + } + String key = join(prefixedStrings, ", "); + return key; + } + + private static String join(Collection things, String separator) { + StringBuilder builder = new StringBuilder(); + boolean first = true; + if (separator == null) { + separator = ""; + } + for (Object thing : things) { + if (!first) { + builder.append(separator); + } + builder.append(thing.toString()); + first = false; + } + return builder.toString(); + } + } + + /** + * Defines a new option. By default, the option takes no arguments and has no description. + * + * @param options The options values. + * @return The option, which can be further configured. + */ + public CommandLineOption option(String... options) { + for (String option : options) { + if (optionsByString.containsKey(option)) { + throw new IllegalArgumentException(String.format("Option '%s' is already defined.", option)); + } + if (option.startsWith("-")) { + throw new IllegalArgumentException(String.format("Cannot add option '%s' as an option cannot start with '-'.", option)); + } + if (!OPTION_NAME_PATTERN.matcher(option).matches()) { + throw new IllegalArgumentException(String.format("Cannot add option '%s' as an option can only contain alphanumeric characters or '-' or '_'.", option)); + } + } + CommandLineOption option = new CommandLineOption(Arrays.asList(options)); + for (String optionStr : option.getOptions()) { + optionsByString.put(optionStr, option); + } + return option; + } + + private static class OptionString { + + private final String arg; + + private final String option; + + private OptionString(String arg, String option) { + this.arg = arg; + this.option = option; + } + + public String getDisplayName() { + return arg.startsWith("--") ? "--" + option : "-" + option; + } + + @Override + public String toString() { + return getDisplayName(); + } + } + + private static abstract class ParserState { + + public abstract boolean maybeStartOption(String arg); + + boolean isOption(String arg) { + return arg.matches("(?s)-.+"); + } + + public abstract OptionParserState onStartOption(String arg, String option); + + public abstract ParserState onNonOption(String arg); + + public void onCommandLineEnd() { + } + } + + private abstract class OptionAwareParserState extends ParserState { + + protected final ParsedCommandLine commandLine; + + protected OptionAwareParserState(ParsedCommandLine commandLine) { + this.commandLine = commandLine; + } + + @Override + public boolean maybeStartOption(String arg) { + return isOption(arg); + } + + @Override + public ParserState onNonOption(String arg) { + commandLine.addExtraValue(arg); + return allowMixedOptions ? new AfterFirstSubCommand(commandLine) : new AfterOptions(commandLine); + } + } + + private class BeforeFirstSubCommand extends OptionAwareParserState { + + private BeforeFirstSubCommand(ParsedCommandLine commandLine) { + super(commandLine); + } + + @Override + public OptionParserState onStartOption(String arg, String option) { + OptionString optionString = new OptionString(arg, option); + CommandLineOption commandLineOption = optionsByString.get(option); + if (commandLineOption == null) { + if (allowUnknownOptions) { + return new UnknownOptionParserState(arg, commandLine, this); + } else { + throw new CommandLineArgumentException(String.format("Unknown command-line option '%s'.", optionString)); + } + } + return new KnownOptionParserState(optionString, commandLineOption, commandLine, this); + } + } + + private class AfterFirstSubCommand extends OptionAwareParserState { + + private AfterFirstSubCommand(ParsedCommandLine commandLine) { + super(commandLine); + } + + @Override + public OptionParserState onStartOption(String arg, String option) { + CommandLineOption commandLineOption = optionsByString.get(option); + if (commandLineOption == null) { + return new UnknownOptionParserState(arg, commandLine, this); + } + return new KnownOptionParserState(new OptionString(arg, option), commandLineOption, commandLine, this); + } + } + + private static class AfterOptions extends ParserState { + + private final ParsedCommandLine commandLine; + + private AfterOptions(ParsedCommandLine commandLine) { + this.commandLine = commandLine; + } + + @Override + public boolean maybeStartOption(String arg) { + return false; + } + + @Override + public OptionParserState onStartOption(String arg, String option) { + return new UnknownOptionParserState(arg, commandLine, this); + } + + @Override + public ParserState onNonOption(String arg) { + commandLine.addExtraValue(arg); + return this; + } + } + + private static class MissingOptionArgState extends ParserState { + + private final OptionParserState option; + + private MissingOptionArgState(OptionParserState option) { + this.option = option; + } + + @Override + public boolean maybeStartOption(String arg) { + return isOption(arg); + } + + @Override + public OptionParserState onStartOption(String arg, String option) { + return this.option.onComplete().onStartOption(arg, option); + } + + @Override + public ParserState onNonOption(String arg) { + return option.onArgument(arg); + } + + @Override + public void onCommandLineEnd() { + option.onComplete(); + } + } + + private static abstract class OptionParserState { + + public abstract ParserState onStartNextArg(); + + public abstract ParserState onArgument(String argument); + + public abstract boolean getHasArgument(); + + public abstract ParserState onComplete(); + } + + private static class KnownOptionParserState extends OptionParserState { + + private final OptionString optionString; + + private final CommandLineOption option; + + private final ParsedCommandLine commandLine; + + private final ParserState state; + + private final List values = new ArrayList(); + + private KnownOptionParserState(OptionString optionString, CommandLineOption option, ParsedCommandLine commandLine, ParserState state) { + this.optionString = optionString; + this.option = option; + this.commandLine = commandLine; + this.state = state; + } + + @Override + public ParserState onArgument(String argument) { + if (!getHasArgument()) { + throw new CommandLineArgumentException(String.format("Command-line option '%s' does not take an argument.", optionString)); + } + if (argument.length() == 0) { + throw new CommandLineArgumentException(String.format("An empty argument was provided for command-line option '%s'.", optionString)); + } + values.add(argument); + return onComplete(); + } + + @Override + public ParserState onStartNextArg() { + if (option.getAllowsArguments() && values.isEmpty()) { + return new MissingOptionArgState(this); + } + return onComplete(); + } + + @Override + public boolean getHasArgument() { + return option.getAllowsArguments(); + } + + @Override + public ParserState onComplete() { + if (getHasArgument() && values.isEmpty()) { + throw new CommandLineArgumentException(String.format("No argument was provided for command-line option '%s' with description: '%s'", optionString, option.getDescription())); + } + ParsedCommandLineOption parsedOption = commandLine.addOption(optionString.option, option); + if (values.size() + parsedOption.getValues().size() > 1 && !option.getAllowsMultipleArguments()) { + throw new CommandLineArgumentException(String.format("Multiple arguments were provided for command-line option '%s'.", optionString)); + } + for (String value : values) { + parsedOption.addArgument(value); + } + for (CommandLineOption otherOption : option.getGroupWith()) { + commandLine.removeOption(otherOption); + } + return state; + } + } + + private static class UnknownOptionParserState extends OptionParserState { + + private final ParserState state; + + private final String arg; + + private final ParsedCommandLine commandLine; + + private UnknownOptionParserState(String arg, ParsedCommandLine commandLine, ParserState state) { + this.arg = arg; + this.commandLine = commandLine; + this.state = state; + } + + @Override + public boolean getHasArgument() { + return true; + } + + @Override + public ParserState onStartNextArg() { + return onComplete(); + } + + @Override + public ParserState onArgument(String argument) { + return onComplete(); + } + + @Override + public ParserState onComplete() { + commandLine.addExtraValue(arg); + return state; + } + } + + private static final class OptionComparator implements Comparator { + + @Override + public int compare(CommandLineOption option1, CommandLineOption option2) { + String min1 = Collections.min(option1.getOptions(), new OptionStringComparator()); + String min2 = Collections.min(option2.getOptions(), new OptionStringComparator()); + // Group opposite option pairs together + min1 = min1.startsWith(DISABLE_OPTION_PREFIX) ? min1.substring(DISABLE_OPTION_PREFIX.length()) + "-" : min1; + min2 = min2.startsWith(DISABLE_OPTION_PREFIX) ? min2.substring(DISABLE_OPTION_PREFIX.length()) + "-" : min2; + return new CaseInsensitiveStringComparator().compare(min1, min2); + } + } + + private static final class CaseInsensitiveStringComparator implements Comparator { + + @Override + public int compare(String option1, String option2) { + int diff = option1.compareToIgnoreCase(option2); + if (diff != 0) { + return diff; + } + return option1.compareTo(option2); + } + } + + private static final class OptionStringComparator implements Comparator { + + @Override + public int compare(String option1, String option2) { + boolean short1 = option1.length() == 1; + boolean short2 = option2.length() == 1; + if (short1 && !short2) { + return 1; + } + if (!short1 && short2) { + return -1; + } + return new CaseInsensitiveStringComparator().compare(option1, option2); + } + } +} + +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +/** + * Categories for build options. Used in the help output. + */ +// @NullMarked +enum OptionCategory { + + HELP("Help"), + LOGGING("Logging"), + CONSOLE("Console"), + CONFIGURATION("Configuration"), + EXECUTION("Execution"), + PERFORMANCE("Performance"), + SECURITY("Security"), + DIAGNOSTICS("Diagnostics"), + DAEMON("Daemon"), + DEVELOCITY("Develocity"), + OTHER(""); + + private final String displayName; + + OptionCategory(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} + +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +class ParsedCommandLine { + + private final Map optionsByString = new HashMap(); + + private final Set presentOptions = new HashSet(); + + private final Set removedOptions = new HashSet(); + + private final List extraArguments = new ArrayList(); + + ParsedCommandLine(Iterable options) { + for (CommandLineOption option : options) { + ParsedCommandLineOption parsedOption = new ParsedCommandLineOption(); + for (String optionStr : option.getOptions()) { + optionsByString.put(optionStr, parsedOption); + } + } + } + + @Override + public String toString() { + return String.format("options: %s, extraArguments: %s, removedOptions: %s", quoteAndJoin(presentOptions), quoteAndJoin(extraArguments), quoteAndJoin(removedOptions)); + } + + private String quoteAndJoin(Iterable strings) { + StringBuilder output = new StringBuilder(); + boolean isFirst = true; + for (String string : strings) { + if (!isFirst) { + output.append(", "); + } + output.append("'"); + output.append(string); + output.append("'"); + isFirst = false; + } + return output.toString(); + } + + /** + * Returns true if the given option is present in this command-line. + * + * @param option The option, without the '-' or '--' prefix. + * @return true if the option is present. + */ + public boolean hasOption(String option) { + option(option); + return presentOptions.contains(option); + } + + /** + * Returns true if the given option was present in this command-line, + * but was removed because another option appeared later that replaces it. + * + * @param option The option, without the '-' or '--' prefix. + * @return true if the option was present. + */ + public boolean hadOptionRemoved(String option) { + option(option); + return removedOptions.contains(option); + } + + /** + * See also {@link #hasOption}. + * + * @param logLevelOptions the options to check + * @return true if any of the passed options is present + */ + public boolean hasAnyOption(Collection logLevelOptions) { + for (String option : logLevelOptions) { + if (hasOption(option)) { + return true; + } + } + return false; + } + + /** + * Returns the value of the given option. + * + * @param option The option, without the '-' or '--' prefix. + * @return The option. never returns null. + */ + public ParsedCommandLineOption option(String option) { + ParsedCommandLineOption parsedOption = optionsByString.get(option); + if (parsedOption == null) { + throw new IllegalArgumentException(String.format("Option '%s' not defined.", option)); + } + return parsedOption; + } + + public List getExtraArguments() { + return extraArguments; + } + + void addExtraValue(String value) { + extraArguments.add(value); + } + + ParsedCommandLineOption addOption(String optionStr, CommandLineOption option) { + ParsedCommandLineOption parsedOption = Objects.requireNonNull(optionsByString.get(optionStr)); + presentOptions.addAll(option.getOptions()); + return parsedOption; + } + + void removeOption(CommandLineOption option) { + for (String optionStr : option.getOptions()) { + if (presentOptions.remove(optionStr)) { + // Only keep track of removed options that were present in the command line + removedOptions.add(optionStr); + } + } + } +} + +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +class ParsedCommandLineOption { + + private final List values = new ArrayList(); + + public String getValue() { + if (!hasValue()) { + throw new IllegalStateException("Option does not have any value."); + } + if (values.size() > 1) { + throw new IllegalStateException("Option has multiple values."); + } + return values.get(0); + } + + public List getValues() { + return values; + } + + public void addArgument(String argument) { + values.add(argument); + } + + public boolean hasValue() { + return !values.isEmpty(); + } +} + +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +class ProjectPropertiesCommandLineConverter extends AbstractPropertiesCommandLineConverter { + + @Override + protected String getPropertyOption() { + return "P"; + } + + @Override + protected String getPropertyOptionDetailed() { + return "project-prop"; + } + + @Override + protected String getPropertyOptionDescription() { + return "Sets a project property for the build script (for example, -Pmyprop=myvalue)."; + } + + @Override + protected OptionCategory getCategory() { + return OptionCategory.CONFIGURATION; + } +} + +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.cli; + +class SystemPropertiesCommandLineConverter extends AbstractPropertiesCommandLineConverter { + + @Override + protected String getPropertyOption() { + return "D"; + } + + @Override + protected String getPropertyOptionDetailed() { + return "system-prop"; + } + + @Override + protected String getPropertyOptionDescription() { + return "Sets a JVM system property (for example, -Dmyprop=myvalue)."; + } + + @Override + protected OptionCategory getCategory() { + return OptionCategory.CONFIGURATION; + } +} + +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.internal.file; + +class PathTraversalChecker { + + private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.US).contains("windows"); + + /** + * Checks the entry name for path traversal vulnerable sequences. + * + * This code is used for path traversal, ZipSlip and TarSlip detection. + * + * IMPLEMENTATION NOTE + * We do it this way instead of the way recommended in + * for performance reasons, calling {@link File#getCanonicalPath()} is too expensive. + * + * @throws IllegalArgumentException if the entry contains vulnerable sequences + */ + public static String safePathName(String name) { + if (isUnsafePathName(name)) { + throw new IllegalArgumentException(format("'%s' is not a safe archive entry or path name.", name)); + } + return name; + } + + public static boolean isUnsafePathName(String name) { + if (name.isEmpty()) { + return true; + } + if (IS_WINDOWS && name.contains(":")) { + return true; + } + if (name.startsWith("/") || name.startsWith("\\")) { + return true; + } + return containsDirectoryNavigation(name); + } + + /** + * We want to treat both '/' and '\' as path separators on all OSes. + * + * @param name the original path name + * @return the path name with all separators replaced with the OS file separator + */ + private static String osIndependentPath(String name) { + if (File.separatorChar == '\\') { + return name.replace('/', File.separatorChar); + } else if (File.separatorChar == '/') { + return name.replace('\\', File.separatorChar); + } else { + // Throw an error here, as we would want to add this separator to our list + // rather than passing it through unmodified + throw new IllegalStateException("Unknown file separator: " + File.separatorChar); + } + } + + private static boolean containsDirectoryNavigation(String name) { + List names = buildNamesList(name); + for (String part : names) { + if (part.equals("..")) { + return true; + } + if (IS_WINDOWS) { + // Directories with dots at the end will have them removed by win32 compatibility + // We don't know what paths might be directories, so just ban any occurrence of dots at the end + if (!part.equals(".") && part.endsWith(".")) { + return true; + } + } + } + return false; + } + + private static List buildNamesList(String name) { + // We run this through File then toPath, as `name` is primarily used with new File(...) calls elsewhere + // This ensures a consistent parsing/understanding of the path + Path path = new File(osIndependentPath(name)).toPath(); + List names = new ArrayList<>(path.getNameCount()); + for (Path part : path) { + names.add(part.toString()); + } + return names; + } +} + +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.internal.file.locking; + +class ExclusiveFileAccessManager { + + public static final String LOCK_FILE_SUFFIX = ".lck"; + + private final int timeoutMs; + + private final int pollIntervalMs; + + public ExclusiveFileAccessManager(int timeoutMs, int pollIntervalMs) { + this.timeoutMs = timeoutMs; + this.pollIntervalMs = pollIntervalMs; + } + + public T access(File exclusiveFile, Callable task) throws Exception { + final File lockFile = new File(exclusiveFile.getParentFile(), exclusiveFile.getName() + LOCK_FILE_SUFFIX); + File lockFileDirectory = lockFile.getParentFile(); + if (!lockFileDirectory.mkdirs() && (!lockFileDirectory.exists() || !lockFileDirectory.isDirectory())) { + throw new RuntimeException("Could not create parent directory for lock file " + lockFile.getAbsolutePath()); + } + RandomAccessFile randomAccessFile = null; + FileChannel channel = null; + try { + long expiry = getTimeMillis() + timeoutMs; + FileLock lock = null; + while (lock == null && getTimeMillis() < expiry) { + randomAccessFile = new RandomAccessFile(lockFile, "rw"); + channel = randomAccessFile.getChannel(); + lock = channel.tryLock(); + if (lock == null) { + maybeCloseQuietly(channel); + maybeCloseQuietly(randomAccessFile); + Thread.sleep(pollIntervalMs); + } + } + if (lock == null) { + throw new RuntimeException("Timeout of " + timeoutMs + " reached waiting for exclusive access to file: " + exclusiveFile.getAbsolutePath()); + } + try { + return task.call(); + } finally { + lock.release(); + maybeCloseQuietly(channel); + channel = null; + maybeCloseQuietly(randomAccessFile); + randomAccessFile = null; + } + } finally { + maybeCloseQuietly(channel); + maybeCloseQuietly(randomAccessFile); + } + } + + private long getTimeMillis() { + return System.nanoTime() / (1000L * 1000L); + } + + private static void maybeCloseQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception ignore) { + // + } + } + } +} + +/* + * Copyright 2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.util.internal; + +// @NullMarked +class ArgumentsSplitter { + + /** + * Splits the arguments string (for example, a program command line) into a collection. + * Only supports space-delimited and/or quoted command line arguments. This currently does not handle escaping characters such as quotes. + * + * @param arguments the arguments, for example command line args. + * @return separate command line arguments. + */ + public static List split(String arguments) { + List commandLineArguments = new ArrayList(); + Character currentQuote = null; + StringBuilder currentOption = new StringBuilder(); + boolean hasOption = false; + for (int index = 0; index < arguments.length(); index++) { + char c = arguments.charAt(index); + if (currentQuote == null && Character.isWhitespace(c)) { + if (hasOption) { + commandLineArguments.add(currentOption.toString()); + hasOption = false; + currentOption.setLength(0); + } + } else if (currentQuote == null && (c == '"' || c == '\'')) { + currentQuote = c; + hasOption = true; + } else if (currentQuote != null && c == currentQuote) { + currentQuote = null; + } else { + currentOption.append(c); + hasOption = true; + } + } + if (hasOption) { + commandLineArguments.add(currentOption.toString()); + } + return commandLineArguments; + } +} + +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.util.internal; + +// @NullMarked +final class WrapperCredentials { + + // @Nullable + private final String token; + + // @Nullable + private final String basicUserInfo; + + private WrapperCredentials(/* // @Nullable */ String token, /* // @Nullable */ String basicUserInfo) { + this.token = token; + this.basicUserInfo = basicUserInfo; + } + + public static WrapperCredentials fromToken(String token) { + return new WrapperCredentials(Objects.requireNonNull(token, "token"), null); + } + + public static WrapperCredentials fromBasicUserInfo(String basicUserInfo) { + return new WrapperCredentials(null, Objects.requireNonNull(basicUserInfo, "basicUserInfo")); + } + + public static WrapperCredentials fromUsernamePassword(String username, String password) { + Objects.requireNonNull(username, "username"); + Objects.requireNonNull(password, "password"); + return fromBasicUserInfo(username + ':' + password); + } + + // @Nullable + public static WrapperCredentials findCredentials(URI distributionUrl, Function propertyProvider) { + Objects.requireNonNull(distributionUrl, "distributionUrl"); + Objects.requireNonNull(propertyProvider, "propertyProvider"); + String token = tryGetProperty(distributionUrl.getHost(), "wrapperToken", propertyProvider); + if (token != null) { + return fromToken(token); + } + return findBasicCredentials(distributionUrl, propertyProvider); + } + + // @Nullable + private static WrapperCredentials findBasicCredentials(URI distributionUrl, Function propertyProvider) { + String host = distributionUrl.getHost(); + String username = tryGetProperty(host, "wrapperUser", propertyProvider); + String password = tryGetProperty(host, "wrapperPassword", propertyProvider); + if (username != null && password != null) { + return fromUsernamePassword(username, password); + } + String userInfo = distributionUrl.getUserInfo(); + return userInfo != null ? fromBasicUserInfo(userInfo) : null; + } + + // @Nullable + private static String tryGetProperty(/* // @Nullable */ String host, String key, Function propertyProvider) { + if (host != null) { + String hostEscaped = host.replace('.', '_').toLowerCase(Locale.ROOT); + String hostProperty = propertyProvider.apply("gradle." + hostEscaped + '.' + key); + if (hostProperty != null) { + return hostProperty; + } + } + return propertyProvider.apply("gradle." + key); + } + + // @Nullable + public String token() { + return token; + } + + private static Map.Entry mapEntry(String key, String value) { + return new AbstractMap.SimpleImmutableEntry<>(key, value); + } + + public Map./* // @Nullable */ Entry usernameAndPassword() { + if (basicUserInfo == null) { + return null; + } + int usernameEnd = basicUserInfo.indexOf(':'); + return usernameEnd >= 0 ? mapEntry(basicUserInfo.substring(0, usernameEnd), basicUserInfo.substring(usernameEnd + 1)) : null; + } + + // @Nullable + public String username() { + Map.Entry combined = usernameAndPassword(); + return combined != null ? combined.getKey() : null; + } + + public String authorizationTypeDisplayName() { + return token != null ? "Bearer Token" : "Basic"; + } + + public Map.Entry authorizationHeader() { + return mapEntry("Authorization", authorizationHeaderValue()); + } + + private String authorizationHeaderValue() { + if (token != null) { + return "Bearer " + token; + } else if (basicUserInfo != null) { + return "Basic " + Base64.getEncoder().encodeToString(basicUserInfo.getBytes(StandardCharsets.UTF_8)); + } else { + throw new AssertionError("Internal error: Unexpected credentials state."); + } + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + WrapperCredentials that = (WrapperCredentials) o; + return Objects.equals(token, that.token) && Objects.equals(basicUserInfo, that.basicUserInfo); + } + + @Override + public int hashCode() { + return Objects.hash(token, basicUserInfo); + } + + @Override + public String toString() { + return "WrapperCredentials{" + (token != null ? "" : "password for " + username()) + '}'; + } +} + +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.util.internal; + +/** + * Converts a wrapper distribution url to a URI. + */ +class WrapperDistributionUrlConverter { + + /** + * Converts the given distribution url to a URI. + *

+ * If the url is relative, it is resolved against the given file root. + * Otherwise, the URI is created from the url. + * + * @param distributionUrl The distribution url. + * @param fileRoot The root directory to resolve relative urls against. + * @return The URI. + * @throws URISyntaxException If the url is not a valid URI. + */ + public static URI convertDistributionUrl(String distributionUrl, File fileRoot) throws URISyntaxException { + URI source = new URI(distributionUrl); + if (source.getScheme() == null) { + // No scheme means someone passed a relative url. + // In our context only file relative urls make sense. + return new File(fileRoot, source.getSchemeSpecificPart()).toURI(); + } else { + return source; + } + } +} + +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class BootstrapMainStarter { + + public void start(String[] args, File gradleHome) throws Exception { + File gradleJar = findLauncherJar(gradleHome); + if (gradleJar == null) { + throw new RuntimeException(String.format("Could not locate the Gradle launcher JAR in Gradle distribution '%s'.", gradleHome)); + } + // The URLClassloader will also include the jars listed in the launcher jar's + // Class-Path manifest attributes as candidates for loading classes. + URLClassLoader contextClassLoader = new URLClassLoader(new URL[] { gradleJar.toURI().toURL() }, ClassLoader.getSystemClassLoader().getParent()); + Thread.currentThread().setContextClassLoader(contextClassLoader); + Class mainClass = contextClassLoader.loadClass("org.gradle.launcher.GradleMain"); + Method mainMethod = mainClass.getMethod("main", String[].class); + mainMethod.invoke(null, new Object[] { args }); + ((Closeable) contextClassLoader).close(); + } + + static File findLauncherJar(File gradleHome) { + File libDirectory = new File(gradleHome, "lib"); + if (libDirectory.exists() && libDirectory.isDirectory()) { + File[] launcherJars = libDirectory.listFiles(new FilenameFilter() { + + @Override + public boolean accept(File dir, String name) { + return name.matches("gradle-launcher-.*\\.jar"); + } + }); + if (launcherJars != null && launcherJars.length == 1) { + return launcherJars[0]; + } + } + return null; + } +} + +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class Download implements IDownload { + + public static final String UNKNOWN_VERSION = "0"; + + public static final int DEFAULT_NETWORK_TIMEOUT_MILLISECONDS = 10 * 1000; + + private static final int BUFFER_SIZE = 10 * 1024; + + private static final int PROGRESS_CHUNK = 1024 * 1024; + + private final Logger logger; + + private final String appName; + + private final String appVersion; + + private final DownloadProgressListener progressListener; + + private final Map systemProperties; + + private final int networkTimeout; + + public Download(Logger logger, String appName, String appVersion) { + this(logger, null, appName, appVersion, convertSystemProperties(System.getProperties())); + } + + public Download(Logger logger, String appName, String appVersion, int networkTimeout) { + this(logger, null, appName, appVersion, convertSystemProperties(System.getProperties()), networkTimeout); + } + + private static Map convertSystemProperties(Properties properties) { + Map result = new HashMap(); + for (Map.Entry entry : properties.entrySet()) { + result.put(entry.getKey().toString(), entry.getValue() == null ? null : entry.getValue().toString()); + } + return result; + } + + public Download(Logger logger, DownloadProgressListener progressListener, String appName, String appVersion, Map systemProperties) { + this(logger, progressListener, appName, appVersion, systemProperties, DEFAULT_NETWORK_TIMEOUT_MILLISECONDS); + } + + public Download(Logger logger, DownloadProgressListener progressListener, String appName, String appVersion, Map systemProperties, int networkTimeout) { + this.logger = logger; + this.appName = appName; + this.appVersion = appVersion; + this.systemProperties = systemProperties; + this.progressListener = new DefaultDownloadProgressListener(logger, progressListener); + this.networkTimeout = networkTimeout; + configureProxyAuthentication(); + } + + private void configureProxyAuthentication() { + if (systemProperties.get("http.proxyUser") != null || systemProperties.get("https.proxyUser") != null) { + // Only an authenticator for proxies needs to be set. Basic authentication is supported by directly setting the request header field. + Authenticator.setDefault(new ProxyAuthenticator(systemProperties)); + } + } + + public void sendHeadRequest(URI uri) throws Exception { + URL safeUrl = safeUri(uri).toURL(); + int responseCode = -1; + try { + HttpURLConnection conn = (HttpURLConnection) safeUrl.openConnection(); + conn.setRequestMethod("HEAD"); + addAuthentication(uri, conn); + conn.setRequestProperty("User-Agent", calculateUserAgent()); + conn.setConnectTimeout(networkTimeout); + conn.setReadTimeout(networkTimeout); + conn.connect(); + responseCode = conn.getResponseCode(); + if (responseCode != 200) { + throw new RuntimeException("HEAD request to " + safeUrl + " failed: response code (" + responseCode + ")"); + } + } catch (IOException e) { + throw new RuntimeException("HEAD request to " + safeUrl + " failed: response code (" + responseCode + "), timeout (" + networkTimeout + "ms)", e); + } + } + + @Override + public void download(URI address, File destination) throws Exception { + destination.getParentFile().mkdirs(); + downloadInternal(address, destination); + } + + private void downloadInternal(URI address, File destination) throws Exception { + OutputStream out = null; + URLConnection conn; + InputStream in = null; + URL safeUrl = safeUri(address).toURL(); + try { + out = new BufferedOutputStream(new FileOutputStream(destination)); + // No proxy is passed here as proxies are set globally using the HTTP(S) proxy system properties. The respective protocol handler implementation then makes use of these properties. + conn = safeUrl.openConnection(); + addAuthentication(address, conn); + final String userAgentValue = calculateUserAgent(); + conn.setRequestProperty("User-Agent", userAgentValue); + conn.setConnectTimeout(networkTimeout); + conn.setReadTimeout(networkTimeout); + // Check HTTP response code before downloading + if (conn instanceof HttpURLConnection) { + HttpURLConnection httpConn = (HttpURLConnection) conn; + int responseCode = httpConn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new IOException("Server returned HTTP response code: " + responseCode + " for URL: " + safeUrl); + } + } + in = conn.getInputStream(); + byte[] buffer = new byte[BUFFER_SIZE]; + int numRead; + int totalLength = conn.getContentLength(); + long downloadedLength = 0; + long progressCounter = 0; + while ((numRead = in.read(buffer)) != -1) { + if (Thread.currentThread().isInterrupted()) { + throw new IOException("Download was interrupted."); + } + downloadedLength += numRead; + progressCounter += numRead; + if (progressCounter / PROGRESS_CHUNK > 0 || downloadedLength == totalLength) { + progressCounter = progressCounter - PROGRESS_CHUNK; + progressListener.downloadStatusChanged(address, totalLength, downloadedLength); + } + out.write(buffer, 0, numRead); + } + } catch (SocketTimeoutException e) { + throw new IOException("Downloading from " + safeUrl + " failed: timeout (" + networkTimeout + "ms)", e); + } finally { + logger.log(""); + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } + } + + /** + * Create a safe URI from the given one by stripping out user info. + * + * @param uri Original URI + * @return a new URI with no user info + */ + static URI safeUri(URI uri) { + try { + return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); + } catch (URISyntaxException e) { + throw new RuntimeException("Failed to parse URI", e); + } + } + + private void addAuthentication(URI address, URLConnection connection) throws IOException { + WrapperCredentials credentials = WrapperCredentials.findCredentials(address, systemProperties::get); + if (credentials == null) { + return; + } + if (!"https".equals(address.getScheme())) { + logger.log("WARNING Using HTTP " + credentials.authorizationTypeDisplayName() + " Authentication over an insecure connection to download the Gradle distribution. Please consider using HTTPS."); + } + Map.Entry authHeader = credentials.authorizationHeader(); + connection.setRequestProperty(authHeader.getKey(), authHeader.getValue()); + } + + private String calculateUserAgent() { + String javaVendor = systemProperties.get("java.vendor"); + String javaVersion = systemProperties.get("java.version"); + String javaVendorVersion = systemProperties.get("java.vm.version"); + String osName = systemProperties.get("os.name"); + String osVersion = systemProperties.get("os.version"); + String osArch = systemProperties.get("os.arch"); + return String.format("%s/%s (%s;%s;%s) (%s;%s;%s)", appName, appVersion, osName, osVersion, osArch, javaVendor, javaVersion, javaVendorVersion); + } + + private static class ProxyAuthenticator extends Authenticator { + + private final Map systemProperties; + + private ProxyAuthenticator(Map systemProperties) { + this.systemProperties = systemProperties; + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + if (getRequestorType() == RequestorType.PROXY) { + // Note: Do not use getRequestingProtocol() here, which is "http" even for HTTPS proxies. + String protocol = getRequestingURL().getProtocol(); + String proxyUser = systemProperties.get(protocol + ".proxyUser"); + if (proxyUser != null) { + String proxyPassword = systemProperties.get(protocol + ".proxyPassword"); + if (proxyPassword == null) { + proxyPassword = ""; + } + return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray()); + } + } + return super.getPasswordAuthentication(); + } + } + + private static class DefaultDownloadProgressListener implements DownloadProgressListener { + + private final Logger logger; + + private final DownloadProgressListener delegate; + + private int previousDownloadPercent; + + public DefaultDownloadProgressListener(Logger logger, DownloadProgressListener delegate) { + this.logger = logger; + this.delegate = delegate; + this.previousDownloadPercent = 0; + } + + @Override + public void downloadStatusChanged(URI address, long contentLength, long downloaded) { + // If the total size of distribution is known, but there's no advanced progress listener, provide extra progress information + if (contentLength > 0 && delegate == null) { + appendPercentageSoFar(contentLength, downloaded); + } + if (contentLength != downloaded) { + logger.append("."); + } + if (delegate != null) { + delegate.downloadStatusChanged(address, contentLength, downloaded); + } + } + + private void appendPercentageSoFar(long contentLength, long downloaded) { + try { + int currentDownloadPercent = 10 * (calculateDownloadPercent(contentLength, downloaded) / 10); + if (currentDownloadPercent != 0 && previousDownloadPercent != currentDownloadPercent) { + logger.append(String.valueOf(currentDownloadPercent)).append('%'); + previousDownloadPercent = currentDownloadPercent; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private int calculateDownloadPercent(long totalLength, long downloadedLength) { + return Math.min(100, Math.max(0, (int) ((downloadedLength / (double) totalLength) * 100))); + } + } +} + +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +interface DownloadProgressListener { + + /** + * Reports the current progress of the download + * + * @param address distribution url + * @param contentLength the content length of the distribution, or -1 if the content length is not known. + * @param downloaded the total amount of currently downloaded bytes + */ + void downloadStatusChanged(URI address, long contentLength, long downloaded); +} + +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class GradleUserHomeLookup { + + public static final String DEFAULT_GRADLE_USER_HOME = System.getProperty("user.home") + "/.gradle"; + + public static final String GRADLE_USER_HOME_PROPERTY_KEY = "gradle.user.home"; + + public static final String GRADLE_USER_HOME_ENV_KEY = "GRADLE_USER_HOME"; + + public static File gradleUserHome() { + String gradleUserHome; + if ((gradleUserHome = System.getProperty(GRADLE_USER_HOME_PROPERTY_KEY)) != null) { + return new File(gradleUserHome); + } + if ((gradleUserHome = System.getenv(GRADLE_USER_HOME_ENV_KEY)) != null) { + return new File(gradleUserHome); + } + return new File(DEFAULT_GRADLE_USER_HOME); + } +} + +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +interface IDownload { + + void download(URI address, File destination) throws Exception; +} + +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class Install { + + public static final String DEFAULT_DISTRIBUTION_PATH = "wrapper/dists"; + + public static final String SHA_256 = ".sha256"; + + public static final int DEFAULT_NETWORK_RETRIES = 0; + + public static final int DEFAULT_NETWORK_RETRY_BACK_OFF_MS = 500; + + private static final int BROKEN_ZIP_RETRIES = 3; + + private final Logger logger; + + private final IDownload download; + + private final PathAssembler pathAssembler; + + private final ExclusiveFileAccessManager exclusiveFileAccessManager = new ExclusiveFileAccessManager(120000, 200); + + public Install(Logger logger, IDownload download, PathAssembler pathAssembler) { + this.logger = logger; + this.download = download; + this.pathAssembler = pathAssembler; + } + + public File createDist(final WrapperConfiguration configuration) throws Exception { + final URI distributionUrl = configuration.getDistribution(); + final PathAssembler.LocalDistribution localDistribution = pathAssembler.getDistribution(configuration); + final File distDir = localDistribution.getDistributionDir(); + final File localZipFile = localDistribution.getZipFile(); + return exclusiveFileAccessManager.access(localZipFile, () -> { + final File markerFile = new File(localZipFile.getParentFile(), localZipFile.getName() + ".ok"); + if (distDir.isDirectory() && markerFile.isFile()) { + InstallCheck installCheck = verifyDistributionRoot(distDir, distDir.getAbsolutePath()); + if (installCheck.isVerified()) { + return installCheck.gradleHome; + } + // Distribution is invalid. Try to reinstall. + System.err.println(installCheck.failureMessage); + markerFile.delete(); + } + fetchDistribution(localZipFile, distributionUrl, distDir, configuration); + InstallCheck installCheck = verifyDistributionRoot(distDir, Download.safeUri(distributionUrl).toASCIIString()); + if (installCheck.isVerified()) { + setExecutablePermissions(installCheck.gradleHome); + markerFile.createNewFile(); + localZipFile.delete(); + return installCheck.gradleHome; + } + // Distribution couldn't be installed. + throw new RuntimeException(installCheck.failureMessage); + }); + } + + private void fetchDistribution(File localZipFile, URI distributionUrl, File distDir, WrapperConfiguration configuration) throws Exception { + List distributionUrls = configuration.getDistributionUrls(); + for (int index = 0; index < distributionUrls.size(); index++) { + URI currentUrl = distributionUrls.get(index); + try { + fetchDistributionFrom(localZipFile, currentUrl, distributionUrl, distDir, configuration); + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw e; + } catch (Exception e) { + if (index + 1 >= distributionUrls.size()) { + throw e; + } + localZipFile.delete(); + deleteLocalTopLevelDirs(distDir); + logger.log("Download from " + Download.safeUri(currentUrl) + " failed. Trying the next download location."); + logger.log("Reason: " + e.getMessage()); + } + } + throw new IllegalStateException("No distribution download locations configured."); + } + + private void fetchDistributionFrom(File localZipFile, URI distributionUrl, URI originalDistributionUrl, File distDir, WrapperConfiguration configuration) throws Exception { + String distributionSha256Sum = configuration.getDistributionSha256Sum(); + boolean failed = false; + boolean originalLocation = distributionUrl.equals(originalDistributionUrl); + int retries = originalLocation ? BROKEN_ZIP_RETRIES : 1; + do { + try { + boolean needsDownload = !localZipFile.isFile() || failed; + if (needsDownload) { + forceFetch(localZipFile, distributionUrl, originalLocation ? configuration.getRetries() : 0, configuration.getRetryBackOffMs()); + } + deleteLocalTopLevelDirs(distDir); + verifyDownloadChecksum(Download.safeUri(distributionUrl).toASCIIString(), localZipFile, distributionSha256Sum); + unzipLocal(localZipFile, distDir); + InstallCheck installCheck = verifyDistributionRoot(distDir, Download.safeUri(distributionUrl).toASCIIString()); + if (!installCheck.isVerified()) { + throw new RuntimeException(installCheck.failureMessage); + } + failed = false; + } catch (ZipException e) { + if (originalLocation && retries >= BROKEN_ZIP_RETRIES && distributionSha256Sum == null) { + distributionSha256Sum = fetchDistributionSha256Sum(configuration, localZipFile); + } + failed = true; + retries--; + if (retries <= 0) { + throw new RuntimeException("Downloaded distribution file " + localZipFile + " is no valid zip file."); + } + } + } while (failed); + } + + private String fetchDistributionSha256Sum(WrapperConfiguration configuration, File localZipFile) { + URI distribution = configuration.getDistribution(); + try { + URI distributionUrl = distribution.resolve(distribution.getPath() + SHA_256); + File tmpZipFile = new File(localZipFile.getParentFile(), localZipFile.getName() + SHA_256); + forceFetch(tmpZipFile, distributionUrl, configuration.getRetries(), configuration.getRetryBackOffMs()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(tmpZipFile.toPath()), StandardCharsets.UTF_8))) { + return reader.readLine(); + } + } catch (Exception e) { + logger.log("Could not fetch hash for " + Download.safeUri(distribution) + "."); + logger.log("Reason: " + e.getMessage()); + return null; + } + } + + private void unzipLocal(File localZipFile, File distDir) throws IOException { + try { + unzip(localZipFile, distDir); + } catch (IOException e) { + logger.log("Could not unzip " + localZipFile.getAbsolutePath() + " to " + distDir.getAbsolutePath() + "."); + logger.log("Reason: " + e.getMessage()); + throw e; + } + } + + private void deleteLocalTopLevelDirs(final File distDir) { + List topLevelDirs = listDirs(distDir); + for (File dir : topLevelDirs) { + logger.log("Deleting directory " + dir.getAbsolutePath()); + deleteDir(dir); + } + } + + private void forceFetch(File localTargetFile, URI distributionUrl, int networkRetries, int networkRetryBackOffMs) throws Exception { + // negative retry parameter values will be handled as the defaults + networkRetries = networkRetries >= 0 ? networkRetries : DEFAULT_NETWORK_RETRIES; + networkRetryBackOffMs = networkRetryBackOffMs >= 0 ? networkRetryBackOffMs : DEFAULT_NETWORK_RETRY_BACK_OFF_MS; + logger.log(String.format("Fetching distribution%s.", networkRetries <= 0 ? "" : String.format(" (retrying %d times, with an initial back off of %d ms)", networkRetries, networkRetryBackOffMs))); + int attempts = networkRetries + 1; + long currentBackOffMs = networkRetryBackOffMs; + Exception lastException = null; + for (int attempt = 1; attempt <= attempts; attempt++) { + try { + File tempDownloadFile = new File(localTargetFile.getParentFile(), localTargetFile.getName() + ".part"); + Files.deleteIfExists(tempDownloadFile.toPath()); + logger.log("Downloading " + Download.safeUri(distributionUrl)); + download.download(distributionUrl, tempDownloadFile); + Files.move(tempDownloadFile.toPath(), localTargetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + return; + } catch (IOException ioException) { + lastException = ioException; + logger.log(String.format("Attempt %d/%d failed. Reason: %s", attempt, attempts, ioException.getMessage())); + if (attempt < attempts) { + Thread.sleep(currentBackOffMs); + currentBackOffMs *= 2; + } + } + } + throw lastException; + } + + static String calculateSha256Sum(File file) throws Exception { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + try (InputStream fis = Files.newInputStream(file.toPath())) { + int n = 0; + byte[] buffer = new byte[4096]; + while (n != -1) { + n = fis.read(buffer); + if (n > 0) { + md.update(buffer, 0, n); + } + } + } + byte[] byteData = md.digest(); + StringBuilder hexString = new StringBuilder(); + for (byte byteDatum : byteData) { + String hex = Integer.toHexString(0xff & byteDatum); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + private InstallCheck verifyDistributionRoot(File distDir, String distributionDescription) { + List dirs = listDirs(distDir); + if (dirs.isEmpty()) { + return InstallCheck.failure(format("Gradle distribution '%s' does not contain any directories. Expected to find exactly 1 directory.", distributionDescription)); + } + if (dirs.size() != 1) { + return InstallCheck.failure(format("Gradle distribution '%s' contains too many directories. Expected to find exactly 1 directory.", distributionDescription)); + } + File gradleHome = dirs.get(0); + if (BootstrapMainStarter.findLauncherJar(gradleHome) == null) { + return InstallCheck.failure(format("Gradle distribution '%s' does not appear to contain a Gradle distribution.", distributionDescription)); + } + return InstallCheck.success(gradleHome); + } + + private void verifyDownloadChecksum(String sourceUrl, File localZipFile, String expectedSum) throws Exception { + if (expectedSum == null) { + return; + } + // if a SHA-256 hash sum has been defined in gradle-wrapper.properties, verify it here + String actualSum = calculateSha256Sum(localZipFile); + if (expectedSum.equalsIgnoreCase(actualSum)) { + return; + } + localZipFile.delete(); + String message = format("Verification of Gradle distribution failed!%n" + "%n" + "Your Gradle distribution may have been tampered with.%n" + "Confirm that the 'distributionSha256Sum' property in your gradle-wrapper.properties file is correct and you are downloading the wrapper from a trusted source.%n" + "%n" + "Distribution Url: %s%n" + "Download Location: %s%n" + "Expected checksum: '%s'%n" + "Actual checksum: '%s'%n" + "Visit https://gradle.org/release-checksums/ to verify the checksums of official distributions. If your build uses a custom distribution, see with its provider.", sourceUrl, localZipFile.getAbsolutePath(), expectedSum, actualSum); + throw new RuntimeException(message); + } + + @SuppressWarnings("MixedMutabilityReturnType") + private List listDirs(File distDir) { + if (!distDir.exists()) { + return emptyList(); + } + File[] files = distDir.listFiles(); + if (files == null) { + return emptyList(); + } + List dirs = new ArrayList<>(); + for (File file : files) { + if (file.isDirectory()) { + dirs.add(file); + } + } + return dirs; + } + + private void setExecutablePermissions(File gradleHome) { + if (isWindows()) { + return; + } + File gradleCommand = new File(gradleHome, "bin/gradle"); + String errorMessage = null; + try { + ProcessBuilder pb = new ProcessBuilder("chmod", "755", gradleCommand.getCanonicalPath()); + Process p = pb.start(); + if (p.waitFor() != 0) { + BufferedReader is = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8)); + Formatter stdout = new Formatter(); + String line; + while ((line = is.readLine()) != null) { + stdout.format("%s%n", line); + } + errorMessage = stdout.toString(); + } + } catch (IOException e) { + errorMessage = e.getMessage(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + errorMessage = e.getMessage(); + } + if (errorMessage != null) { + logger.log("Could not set executable permissions for: " + gradleCommand.getAbsolutePath()); + } + } + + private boolean isWindows() { + String osName = System.getProperty("os.name").toLowerCase(Locale.US); + return osName.contains("windows"); + } + + private boolean deleteDir(File dir) { + if (dir.isDirectory()) { + String[] children = dir.list(); + if (children != null) { + for (String child : children) { + boolean success = deleteDir(new File(dir, child)); + if (!success) { + return false; + } + } + } + } + // The directory is now empty so delete it + return dir.delete(); + } + + private void unzip(File zip, File dest) throws IOException { + try (ZipFile zipFile = new ZipFile(zip)) { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File destFile = new File(dest, PathTraversalChecker.safePathName(entry.getName())); + if (entry.isDirectory()) { + destFile.mkdirs(); + continue; + } + try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(destFile.toPath()))) { + copyInputStream(zipFile.getInputStream(entry), outputStream); + } + } + } + } + + private void copyInputStream(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) >= 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + } + + private static class InstallCheck { + + private final File gradleHome; + + private final String failureMessage; + + private static InstallCheck failure(String message) { + return new InstallCheck(null, message); + } + + private static InstallCheck success(File gradleHome) { + return new InstallCheck(gradleHome, null); + } + + private InstallCheck(File gradleHome, String failureMessage) { + this.gradleHome = gradleHome; + this.failureMessage = failureMessage; + } + + private boolean isVerified() { + return gradleHome != null; + } + } +} + +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class Logger implements Appendable { + + private final boolean quiet; + + public Logger(boolean quiet) { + this.quiet = quiet; + } + + public void log(String message) { + if (!quiet) { + System.out.println(message); + } + } + + @Override + public Appendable append(CharSequence csq) { + if (!quiet) { + System.out.append(csq); + } + return this; + } + + @Override + public Appendable append(CharSequence csq, int start, int end) { + if (!quiet) { + System.out.append(csq, start, end); + } + return this; + } + + @Override + public Appendable append(char c) { + if (!quiet) { + System.out.append(c); + } + return this; + } +} + +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +/** + * Parses JSON without external runtime dependencies. + * + *

Objects preserve member order, numbers are represented by {@link BigDecimal}, duplicate + * object keys are rejected, and nesting is limited to protect Wrapper startup.

+ */ +final class MinimalJsonParser { + + private static final int MAX_DEPTH = 64; + + private MinimalJsonParser() { + } + + /** + * Parses one complete JSON value. + * + * @param input the JSON text + * @return the parsed JSON value + * @throws IllegalArgumentException if the input is malformed + */ + static Object parse(String input) { + return new Parser(input).parse(); + } + + private static final class Parser { + + private final String input; + + private int offset; + + private Parser(String input) { + this.input = input; + if (!input.isEmpty() && input.charAt(0) == '\ufeff') { + offset = 1; + } + } + + private Object parse() { + skipWhitespace(); + Object value = parseValue(0); + skipWhitespace(); + if (offset != input.length()) { + throw error("Unexpected trailing content"); + } + return value; + } + + private Object parseValue(int depth) { + if (depth > MAX_DEPTH) { + throw error("Maximum nesting depth exceeded"); + } + if (offset >= input.length()) { + throw error("Expected a value"); + } + char current = input.charAt(offset); + switch(current) { + case '{': + return parseObject(depth + 1); + case '[': + return parseArray(depth + 1); + case '"': + return parseString(); + case 't': + expectLiteral("true"); + return Boolean.TRUE; + case 'f': + expectLiteral("false"); + return Boolean.FALSE; + case 'n': + expectLiteral("null"); + return null; + default: + if (current == '-' || isDigit(current)) { + return parseNumber(); + } + throw error("Unexpected character '" + current + "'"); + } + } + + private Map parseObject(int depth) { + offset++; + skipWhitespace(); + Map result = new LinkedHashMap<>(); + if (consume('}')) { + return result; + } + while (true) { + if (offset >= input.length() || input.charAt(offset) != '"') { + throw error("Expected an object key"); + } + String key = parseString(); + if (result.containsKey(key)) { + throw error("Duplicate object key '" + key + "'"); + } + skipWhitespace(); + expect(':'); + skipWhitespace(); + result.put(key, parseValue(depth)); + skipWhitespace(); + if (consume('}')) { + return result; + } + expect(','); + skipWhitespace(); + } + } + + private List parseArray(int depth) { + offset++; + skipWhitespace(); + List result = new ArrayList<>(); + if (consume(']')) { + return result; + } + while (true) { + result.add(parseValue(depth)); + skipWhitespace(); + if (consume(']')) { + return result; + } + expect(','); + skipWhitespace(); + } + } + + private String parseString() { + offset++; + StringBuilder result = new StringBuilder(); + while (offset < input.length()) { + char current = input.charAt(offset++); + if (current == '"') { + return result.toString(); + } + if (current == '\\') { + if (offset >= input.length()) { + throw error("Unterminated escape sequence"); + } + char escaped = input.charAt(offset++); + switch(escaped) { + case '"': + case '\\': + case '/': + result.append(escaped); + break; + case 'b': + result.append('\b'); + break; + case 'f': + result.append('\f'); + break; + case 'n': + result.append('\n'); + break; + case 'r': + result.append('\r'); + break; + case 't': + result.append('\t'); + break; + case 'u': + result.append(parseUnicodeEscape()); + break; + default: + throw error("Invalid escape sequence '\\" + escaped + "'"); + } + } else { + if (current < 0x20) { + throw error("Unescaped control character in string"); + } + result.append(current); + } + } + throw error("Unterminated string"); + } + + private char parseUnicodeEscape() { + if (offset + 4 > input.length()) { + throw error("Incomplete Unicode escape"); + } + int value = 0; + for (int i = 0; i < 4; i++) { + char current = input.charAt(offset++); + int digit = Character.digit(current, 16); + if (digit < 0) { + throw error("Invalid Unicode escape"); + } + value = (value << 4) | digit; + } + return (char) value; + } + + private BigDecimal parseNumber() { + int start = offset; + consume('-'); + if (consume('0')) { + if (offset < input.length() && isDigit(input.charAt(offset))) { + throw error("Leading zero in number"); + } + } else { + requireDigit(); + consumeDigits(); + } + if (consume('.')) { + requireDigit(); + consumeDigits(); + } + if (consume('e') || consume('E')) { + if (!consume('+')) { + consume('-'); + } + requireDigit(); + consumeDigits(); + } + try { + return new BigDecimal(input.substring(start, offset)); + } catch (NumberFormatException e) { + throw error("Invalid number"); + } + } + + private void consumeDigits() { + while (offset < input.length() && isDigit(input.charAt(offset))) { + offset++; + } + } + + private void requireDigit() { + if (offset >= input.length() || !isDigit(input.charAt(offset))) { + throw error("Expected a digit"); + } + } + + private void expectLiteral(String literal) { + if (!input.regionMatches(offset, literal, 0, literal.length())) { + throw error("Expected '" + literal + "'"); + } + offset += literal.length(); + } + + private void expect(char expected) { + if (!consume(expected)) { + throw error("Expected '" + expected + "'"); + } + } + + private boolean consume(char expected) { + if (offset < input.length() && input.charAt(offset) == expected) { + offset++; + return true; + } + return false; + } + + private void skipWhitespace() { + while (offset < input.length()) { + char current = input.charAt(offset); + if (current != ' ' && current != '\t' && current != '\r' && current != '\n') { + return; + } + offset++; + } + } + + private IllegalArgumentException error(String message) { + return new IllegalArgumentException("Invalid JSON at offset " + offset + ": " + message); + } + + private static boolean isDigit(char value) { + return value >= '0' && value <= '9'; + } + } +} + +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +/** + * Loads user-level mirror rules and resolves ordered distribution download candidates. + * + *

Mirror results precede the original distribution URL, which is always retained as the final + * fallback. Rules that require a checksum are skipped when no checksum is configured.

+ */ +final class MirrorConfiguration { + + /** + * Default configuration file name within the Gradle user home. + */ + static final String FILE_NAME = "gradle-wrapper-neo.json"; + + /** + * Environment variable that overrides the configuration file location. + */ + static final String CONFIGURATION_ENV = "GRADLE_WRAPPER_NEO_CONFIG"; + + private static final int MAX_FILE_SIZE = 1024 * 1024; + + private static final Set ROOT_FIELDS = fields("version", "mirrors"); + + private static final Set MIRROR_FIELDS = fields("enabled", "pattern", "replacement", "requireChecksum"); + + private final List mirrors; + + private MirrorConfiguration(List mirrors) { + this.mirrors = Collections.unmodifiableList(new ArrayList<>(mirrors)); + } + + /** + * Returns a configuration that resolves only the original distribution URL. + * @return an empty mirror configuration + */ + static MirrorConfiguration empty() { + return new MirrorConfiguration(Collections.emptyList()); + } + + /** + * Loads the effective configuration file for a Gradle user home. + * + * @param gradleUserHome the effective Gradle user home + * @return the parsed mirror configuration, or an empty configuration when the file is absent + */ + static MirrorConfiguration load(File gradleUserHome) { + File file = configurationFile(gradleUserHome, System.getenv(CONFIGURATION_ENV)); + return loadFile(file); + } + + /** + * Resolves the configuration file from the default home and an optional environment override. + * + * @param gradleUserHome the effective Gradle user home + * @param configuredPath the environment override, or {@code null} + * @return the effective configuration file + */ + static File configurationFile(File gradleUserHome, String configuredPath) { + if (configuredPath == null) { + return new File(gradleUserHome, FILE_NAME); + } + if (configuredPath.isEmpty()) { + throw new IllegalArgumentException("Environment variable " + CONFIGURATION_ENV + " must not be empty."); + } + File file = new File(configuredPath); + if (!file.isAbsolute()) { + throw new IllegalArgumentException("Environment variable " + CONFIGURATION_ENV + " must contain an absolute path: " + configuredPath); + } + return file; + } + + private static MirrorConfiguration loadFile(File file) { + if (!file.exists()) { + return empty(); + } + if (!file.isFile()) { + throw new RuntimeException("Mirror configuration is not a file: " + file); + } + try { + if (file.length() > MAX_FILE_SIZE) { + throw new IllegalArgumentException("Mirror configuration exceeds " + MAX_FILE_SIZE + " bytes."); + } + byte[] content = Files.readAllBytes(file.toPath()); + if (content.length > MAX_FILE_SIZE) { + throw new IllegalArgumentException("Mirror configuration exceeds " + MAX_FILE_SIZE + " bytes."); + } + String json = new String(content, StandardCharsets.UTF_8); + return parse(json); + } catch (IOException | RuntimeException e) { + throw new RuntimeException("Could not load mirror configuration from '" + file + "'.", e); + } + } + + /** + * Parses and validates a Wrapper Neo mirror configuration. + * + * @param json the JSON configuration text + * @return the validated mirror configuration + */ + static MirrorConfiguration parse(String json) { + Map root = object(MinimalJsonParser.parse(json), "root"); + rejectUnknownFields(root, ROOT_FIELDS, "root"); + BigDecimal version = number(required(root, "version", "root"), "root.version"); + if (version.compareTo(BigDecimal.ONE) != 0) { + throw new IllegalArgumentException("Unsupported mirror configuration version: " + version); + } + List mirrors = new ArrayList<>(); + if (root.containsKey("mirrors")) { + Object configuredMirrors = root.get("mirrors"); + List entries = array(configuredMirrors, "root.mirrors"); + for (int index = 0; index < entries.size(); index++) { + Mirror mirror = parseMirror(entries.get(index), "root.mirrors[" + index + "]"); + if (mirror != null) { + mirrors.add(mirror); + } + } + } + return new MirrorConfiguration(mirrors); + } + + /** + * Resolves mirror candidates for a distribution URL. + * + * @param source the original distribution URL + * @param checksumProvided whether the distribution has an expected SHA-256 checksum + * @return distinct candidate URLs in attempt order, ending with {@code source} + */ + List resolve(URI source, boolean checksumProvided) { + LinkedHashSet result = new LinkedHashSet<>(); + for (Mirror mirror : mirrors) { + URI resolved = mirror.resolve(source, checksumProvided); + if (resolved != null && !resolved.equals(source)) { + result.add(resolved); + } + } + result.add(source); + return new ArrayList<>(result); + } + + private static Mirror parseMirror(Object value, String path) { + Map mirror = object(value, path); + rejectUnknownFields(mirror, MIRROR_FIELDS, path); + if (!optionalBoolean(mirror, "enabled", true, path)) { + return null; + } + String patternText = string(required(mirror, "pattern", path), path + ".pattern"); + Pattern pattern; + try { + pattern = Pattern.compile(patternText); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Invalid regular expression at " + path + ".pattern: " + e.getDescription(), e); + } + String replacement = string(required(mirror, "replacement", path), path + ".replacement"); + boolean requireChecksum = optionalBoolean(mirror, "requireChecksum", true, path); + return new Mirror(pattern, replacement, requireChecksum); + } + + private static Object required(Map object, String field, String path) { + if (!object.containsKey(field)) { + throw new IllegalArgumentException("Missing required field " + path + "." + field + "."); + } + return object.get(field); + } + + private static boolean optionalBoolean(Map object, String field, boolean defaultValue, String path) { + if (!object.containsKey(field)) { + return defaultValue; + } + Object value = object.get(field); + if (!(value instanceof Boolean)) { + throw new IllegalArgumentException(path + "." + field + " must be a boolean."); + } + return (Boolean) value; + } + + @SuppressWarnings("unchecked") + private static Map object(Object value, String path) { + if (!(value instanceof Map)) { + throw new IllegalArgumentException(path + " must be an object."); + } + return (Map) value; + } + + @SuppressWarnings("unchecked") + private static List array(Object value, String path) { + if (!(value instanceof List)) { + throw new IllegalArgumentException(path + " must be an array."); + } + return (List) value; + } + + private static String string(Object value, String path) { + if (!(value instanceof String)) { + throw new IllegalArgumentException(path + " must be a string."); + } + return (String) value; + } + + private static BigDecimal number(Object value, String path) { + if (!(value instanceof BigDecimal)) { + throw new IllegalArgumentException(path + " must be a number."); + } + return (BigDecimal) value; + } + + private static void rejectUnknownFields(Map object, Set allowedFields, String path) { + for (String field : object.keySet()) { + if (!allowedFields.contains(field)) { + throw new IllegalArgumentException("Unknown field " + path + "." + field + "."); + } + } + } + + private static Set fields(String... values) { + Set result = new HashSet<>(); + Collections.addAll(result, values); + return Collections.unmodifiableSet(result); + } + + private static final class Mirror { + + private final Pattern pattern; + + private final String replacement; + + private final boolean requireChecksum; + + private Mirror(Pattern pattern, String replacement, boolean requireChecksum) { + this.pattern = pattern; + this.replacement = replacement; + this.requireChecksum = requireChecksum; + } + + private URI resolve(URI source, boolean checksumProvided) { + if (requireChecksum && !checksumProvided) { + return null; + } + Matcher matcher = pattern.matcher(source.toASCIIString()); + if (!matcher.matches()) { + return null; + } + String resolvedText; + try { + resolvedText = matcher.replaceFirst(replacement); + } catch (IllegalArgumentException | IndexOutOfBoundsException e) { + throw new IllegalArgumentException("Invalid mirror replacement '" + replacement + "'.", e); + } + URI resolved; + try { + resolved = URI.create(resolvedText); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Mirror replacement produced an invalid URI: " + resolvedText, e); + } + if (!resolved.isAbsolute() || !"https".equalsIgnoreCase(resolved.getScheme()) || resolved.getHost() == null || resolved.getFragment() != null) { + throw new IllegalArgumentException("Mirror replacement must produce an absolute HTTPS URI without a fragment: " + resolvedText); + } + return resolved; + } + } +} + +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class PathAssembler { + + public static final String GRADLE_USER_HOME_STRING = "GRADLE_USER_HOME"; + + public static final String PROJECT_STRING = "PROJECT"; + + private final File gradleUserHome; + + private final File projectDirectory; + + public PathAssembler(File gradleUserHome, File projectDirectory) { + this.gradleUserHome = gradleUserHome; + this.projectDirectory = projectDirectory; + } + + /** + * Determines the local locations for the distribution to use given the supplied configuration. + */ + public LocalDistribution getDistribution(WrapperConfiguration configuration) { + String baseName = getDistName(configuration.getDistribution()); + String distName = removeExtension(baseName); + String rootDirName = rootDirName(distName, configuration); + File distDir = new File(getBaseDir(configuration.getDistributionBase()), configuration.getDistributionPath() + "/" + rootDirName); + File distZip = new File(getBaseDir(configuration.getZipBase()), configuration.getZipPath() + "/" + rootDirName + "/" + baseName); + return new LocalDistribution(distDir, distZip); + } + + private String rootDirName(String distName, WrapperConfiguration configuration) { + String urlHash = getHash(Download.safeUri(configuration.getDistribution()).toASCIIString()); + return distName + "/" + urlHash; + } + + /** + * This method computes a hash of the provided {@code string}. + *

+ * The algorithm in use by this method is as follows: + *

    + *
  1. Compute the MD5 value of the UTF-8 {@code string}.
  2. + *
  3. Truncate leading zeros (i.e., treat the MD5 value as a number).
  4. + *
  5. Convert to base 36 (the characters {@code 0-9a-z}).
  6. + *
+ */ + @SuppressWarnings("StringCharset") + private String getHash(String string) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + byte[] bytes = string.getBytes("UTF-8"); + messageDigest.update(bytes); + return new BigInteger(1, messageDigest.digest()).toString(36); + } catch (Exception e) { + throw new RuntimeException("Could not hash input string.", e); + } + } + + private String removeExtension(String name) { + int p = name.lastIndexOf("."); + if (p < 0) { + return name; + } + return name.substring(0, p); + } + + private String getDistName(URI distUrl) { + String path = distUrl.getPath(); + int p = path.lastIndexOf("/"); + if (p < 0) { + return path; + } + return path.substring(p + 1); + } + + private File getBaseDir(String base) { + if (base.equals(GRADLE_USER_HOME_STRING)) { + return gradleUserHome; + } else if (base.equals(PROJECT_STRING)) { + return projectDirectory; + } else { + throw new RuntimeException("Base: " + base + " is unknown"); + } + } + + public static class LocalDistribution { + + private final File distZip; + + private final File distDir; + + public LocalDistribution(File distDir, File distZip) { + this.distDir = distDir; + this.distZip = distZip; + } + + /** + * Returns the location to install the distribution into. + */ + public File getDistributionDir() { + return distDir; + } + + /** + * Returns the location to install the distribution ZIP file to. + */ + public File getZipFile() { + return distZip; + } + } +} + +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class PropertiesFileHandler { + + private static final String SYSTEM_PROP_PREFIX = "systemProp."; + + private static final String JVMARGS_PROP_KEY = "org.gradle.jvmargs"; + + private static final String DEBUG_PROP_KEY = "org.gradle.debug"; + + private static final String DEBUG_PROP_VALUE = "true"; + + public static Map getSystemProperties(File propertiesFile) { + if (!propertiesFile.isFile()) { + return Collections.emptyMap(); + } + Properties properties = loadProperties(propertiesFile); + Map systemProperties = new HashMap(); + for (Object argument : properties.keySet()) { + if (argument.toString().startsWith(SYSTEM_PROP_PREFIX)) { + String key = argument.toString().substring(SYSTEM_PROP_PREFIX.length()); + if (key.length() > 0) { + systemProperties.put(key, properties.get(argument).toString()); + } + } + } + return Collections.unmodifiableMap(systemProperties); + } + + public static List getJvmArgs(File propertiesFile) { + if (!propertiesFile.isFile()) { + return Collections.emptyList(); + } + Properties properties = loadProperties(propertiesFile); + List jvmArgs = new ArrayList(); + for (Map.Entry entry : properties.entrySet()) { + if (JVMARGS_PROP_KEY.equals(entry.getKey())) { + Object jvmArgsPropValue = entry.getValue(); + if (jvmArgsPropValue instanceof String) { + jvmArgs.addAll(ArgumentsSplitter.split((String) jvmArgsPropValue)); + } + } else if (DEBUG_PROP_KEY.equals(entry.getKey()) && DEBUG_PROP_VALUE.equals(entry.getValue())) { + jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"); + } + } + return Collections.unmodifiableList(jvmArgs); + } + + private static Properties loadProperties(File propertiesFile) { + Properties properties = new Properties(); + try { + FileInputStream inStream = new FileInputStream(propertiesFile); + try { + properties.load(inStream); + } finally { + inStream.close(); + } + } catch (IOException e) { + throw new RuntimeException("Error when loading properties file=" + propertiesFile, e); + } + return properties; + } +} + +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class WrapperConfiguration { + + private URI distribution; + + private String distributionBase = PathAssembler.GRADLE_USER_HOME_STRING; + + private String distributionPath = Install.DEFAULT_DISTRIBUTION_PATH; + + private String distributionSha256Sum; + + private String zipBase = PathAssembler.GRADLE_USER_HOME_STRING; + + private String zipPath = Install.DEFAULT_DISTRIBUTION_PATH; + + private int networkTimeout = Download.DEFAULT_NETWORK_TIMEOUT_MILLISECONDS; + + private boolean validateDistributionUrl = true; + + private int retries = Install.DEFAULT_NETWORK_RETRIES; + + private int retryBackOffMs = Install.DEFAULT_NETWORK_RETRY_BACK_OFF_MS; + + private MirrorConfiguration mirrorConfiguration = MirrorConfiguration.empty(); + + public URI getDistribution() { + return distribution; + } + + public void setDistribution(URI distribution) { + this.distribution = distribution; + } + + List getDistributionUrls() { + return mirrorConfiguration.resolve(distribution, distributionSha256Sum != null); + } + + void setMirrorConfiguration(MirrorConfiguration mirrorConfiguration) { + this.mirrorConfiguration = mirrorConfiguration; + } + + public String getDistributionBase() { + return distributionBase; + } + + public void setDistributionBase(String distributionBase) { + this.distributionBase = distributionBase; + } + + public String getDistributionPath() { + return distributionPath; + } + + public void setDistributionPath(String distributionPath) { + this.distributionPath = distributionPath; + } + + public void setRetries(int retries) { + this.retries = retries; + } + + public int getRetries() { + return retries; + } + + public void setRetryBackOffMs(int retryBackOffMs) { + this.retryBackOffMs = retryBackOffMs; + } + + public int getRetryBackOffMs() { + return retryBackOffMs; + } + + public String getDistributionSha256Sum() { + return distributionSha256Sum; + } + + public void setDistributionSha256Sum(String distributionSha256Sum) { + this.distributionSha256Sum = distributionSha256Sum; + } + + public String getZipBase() { + return zipBase; + } + + public void setZipBase(String zipBase) { + this.zipBase = zipBase; + } + + public String getZipPath() { + return zipPath; + } + + public void setZipPath(String zipPath) { + this.zipPath = zipPath; + } + + public int getNetworkTimeout() { + return networkTimeout; + } + + public void setNetworkTimeout(int networkTimeout) { + this.networkTimeout = networkTimeout; + } + + public boolean getValidateDistributionUrl() { + return validateDistributionUrl; + } + + public void setValidateDistributionUrl(boolean validateDistributionUrl) { + this.validateDistributionUrl = validateDistributionUrl; + } +} + +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper; + +class WrapperExecutor { + + public static final String DISTRIBUTION_URL_PROPERTY = "distributionUrl"; + + public static final String DISTRIBUTION_BASE_PROPERTY = "distributionBase"; + + public static final String DISTRIBUTION_PATH_PROPERTY = "distributionPath"; + + public static final String DISTRIBUTION_SHA_256_SUM = "distributionSha256Sum"; + + public static final String ZIP_STORE_BASE_PROPERTY = "zipStoreBase"; + + public static final String ZIP_STORE_PATH_PROPERTY = "zipStorePath"; + + public static final String NETWORK_TIMEOUT_PROPERTY = "networkTimeout"; + + public static final String VALIDATE_DISTRIBUTION_URL = "validateDistributionUrl"; + + public static final String RETRIES_PROPERTY = "retries"; + + public static final String RETRY_BACK_OFF_PROPERTY = "retryBackOffMs"; + + private final Properties properties; + + private final File propertiesFile; + + private final WrapperConfiguration config = new WrapperConfiguration(); + + public static File wrapperPropertiesForProjectDirectory(File projectDir) { + return new File(projectDir, "gradle/wrapper/gradle-wrapper.properties"); + } + + public static WrapperExecutor forProjectDirectory(File projectDir) { + return new WrapperExecutor(wrapperPropertiesForProjectDirectory(projectDir), new Properties()); + } + + public static WrapperExecutor forWrapperPropertiesFile(File propertiesFile) { + if (!propertiesFile.exists()) { + throw new RuntimeException(String.format("Wrapper properties file '%s' does not exist.", propertiesFile)); + } + return new WrapperExecutor(propertiesFile, new Properties()); + } + + WrapperExecutor(File propertiesFile, Properties properties) { + this.properties = properties; + this.propertiesFile = propertiesFile; + if (propertiesFile.exists()) { + try { + loadProperties(propertiesFile, properties); + config.setDistribution(WrapperDistributionUrlConverter.convertDistributionUrl(readDistroUrl(), propertiesFile.getParentFile())); + config.setDistributionBase(getProperty(DISTRIBUTION_BASE_PROPERTY, config.getDistributionBase())); + config.setDistributionPath(getProperty(DISTRIBUTION_PATH_PROPERTY, config.getDistributionPath())); + config.setDistributionSha256Sum(getProperty(DISTRIBUTION_SHA_256_SUM, config.getDistributionSha256Sum(), false)); + config.setZipBase(getProperty(ZIP_STORE_BASE_PROPERTY, config.getZipBase())); + config.setZipPath(getProperty(ZIP_STORE_PATH_PROPERTY, config.getZipPath())); + config.setNetworkTimeout(getProperty(NETWORK_TIMEOUT_PROPERTY, config.getNetworkTimeout())); + config.setValidateDistributionUrl(getProperty(VALIDATE_DISTRIBUTION_URL, config.getValidateDistributionUrl())); + config.setRetries(getProperty(RETRIES_PROPERTY, config.getRetries())); + config.setRetryBackOffMs(getProperty(RETRY_BACK_OFF_PROPERTY, config.getRetryBackOffMs())); + } catch (Exception e) { + throw new RuntimeException(String.format("Could not load wrapper properties from '%s'.", propertiesFile), e); + } + } + } + + private String readDistroUrl() { + if (properties.getProperty(DISTRIBUTION_URL_PROPERTY) == null) { + reportMissingProperty(DISTRIBUTION_URL_PROPERTY); + } + return getProperty(DISTRIBUTION_URL_PROPERTY); + } + + private static void loadProperties(File propertiesFile, Properties properties) throws IOException { + InputStream inStream = new FileInputStream(propertiesFile); + try { + properties.load(inStream); + } finally { + inStream.close(); + } + } + + /** + * Returns the distribution which this wrapper will use. Returns null if no wrapper meta-data was found in the specified project directory. + */ + public URI getDistribution() { + return config.getDistribution(); + } + + /** + * Returns the configuration for this wrapper. + */ + public WrapperConfiguration getConfiguration() { + return config; + } + + public void execute(String[] args, Install install, BootstrapMainStarter bootstrapMainStarter) throws Exception { + File gradleHome = install.createDist(config); + bootstrapMainStarter.start(args, gradleHome); + } + + private String getProperty(String propertyName) { + return getProperty(propertyName, null, true); + } + + private String getProperty(String propertyName, String defaultValue) { + return getProperty(propertyName, defaultValue, true); + } + + private int getProperty(String propertyName, int defaultValue) { + return Integer.parseInt(getProperty(propertyName, String.valueOf(defaultValue))); + } + + private boolean getProperty(String propertyName, boolean defaultValue) { + return Boolean.parseBoolean(getProperty(propertyName, String.valueOf(defaultValue))); + } + + private String getProperty(String propertyName, String defaultValue, boolean required) { + String value = properties.getProperty(propertyName); + if (value != null) { + return value; + } + if (defaultValue != null) { + return defaultValue; + } + if (required) { + return reportMissingProperty(propertyName); + } else { + return null; + } + } + + private String reportMissingProperty(String propertyName) { + throw new RuntimeException(String.format("No value with key '%s' specified in wrapper properties file '%s'.", propertyName, propertiesFile)); + } +} + +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// package org.gradle.wrapper.neo; + +/** + * Compiles the source-based Wrapper into a cached JAR and relaunches it when the source changes. + * + *

The launcher supplies the project home, source file, and target JAR as absolute system + * properties. A file lock serializes cache updates across concurrent Wrapper processes.

+ */ +final class Bootstrap { + + private static final String MAIN_CLASS_NAME = "GradleWrapperNeo"; + + private static final String TEMPORARY_JAR_PREFIX = "gradle-wrapper-neo-"; + + private static final String CLASSES_DIR_NAME = "classes"; + + private static final String LOCK_FILE_NAME = "lock"; + + private static final String BOOTSTRAP_PROPERTY = "gradle.wrapper.neo.bootstrap"; + + /** + * System property containing the absolute project directory used as the Wrapper application home. + */ + public static final String APP_HOME_PROPERTY = "org.gradle.wrapper.neo.app-home"; + + /** + * System property containing the absolute path to {@code GradleWrapperNeo.java}. + */ + public static final String SOURCE_FILE_PROPERTY = "org.gradle.wrapper.neo.source-file"; + + /** + * System property containing the absolute path to the cached Wrapper JAR. + */ + public static final String JAR_FILE_PROPERTY = "org.gradle.wrapper.neo.jar-file"; + + private static final String MANIFEST_SOURCE_SHA256 = "Gradle-Wrapper-Neo-Source-SHA256"; + + private Bootstrap() { + } + + /** + * Ensures the cached Wrapper JAR matches the configured source and relaunches it when needed. + * + * @param args the command-line arguments forwarded to the Wrapper + * @param mainClass the class used to locate the currently running code + * @return {@code true} when the caller must stop because another Wrapper process was launched + * @throws Exception if the source cannot be compiled, cached, or launched + */ + public static boolean handle(String[] args, Class mainClass) throws Exception { + Path appHome = appHome(); + Path sourceFile = sourceFile(); + Path targetJar = jarFile(); + if (!Files.isDirectory(appHome)) { + throw new RuntimeException("Application home directory '" + appHome + "' does not exist."); + } + if (!Files.isRegularFile(sourceFile)) { + throw new RuntimeException("Wrapper source file '" + sourceFile + "' does not exist."); + } + if (Boolean.getBoolean(BOOTSTRAP_PROPERTY)) { + Path stagingClassesDir = codeSource(mainClass); + if (!Files.isDirectory(stagingClassesDir)) { + throw new RuntimeException("Bootstrap classes directory '" + stagingClassesDir + "' does not exist."); + } + int exitCode; + try { + withLock(targetJar.getParent(), () -> { + if (!isCurrent(targetJar, sourceFile)) { + Path classesDir = classesDir(targetJar); + recreateDirectory(classesDir); + copyDirectory(stagingClassesDir, classesDir); + writeJar(sourceFile, classesDir, targetJar); + } + return null; + }); + exitCode = launchJar(targetJar, appHome, sourceFile, targetJar, args); + } finally { + deleteRecursively(stagingClassesDir); + deleteIfEmpty(stagingClassesDir.getParent()); + } + System.exit(exitCode); + return true; + } + Path currentJar = codeSource(mainClass); + if (!Files.isRegularFile(currentJar)) { + throw new RuntimeException("Cached wrapper JAR '" + currentJar + "' does not exist."); + } + if (!isExpectedJar(currentJar, targetJar, sourceFile)) { + throw new RuntimeException("Running wrapper JAR '" + currentJar + "' does not match configured JAR '" + targetJar + "'."); + } + if (isCurrent(currentJar, sourceFile)) { + return false; + } + Path classesDir = classesDir(targetJar); + Files.createDirectories(targetJar.getParent()); + Path nextJar = Files.createTempFile(targetJar.getParent(), TEMPORARY_JAR_PREFIX, ".jar"); + Path launchJar; + try { + launchJar = withLock(targetJar.getParent(), () -> { + if (isCurrent(targetJar, sourceFile)) { + return targetJar; + } + compileSource(sourceFile, classesDir); + writeJar(sourceFile, classesDir, nextJar); + return installReplacement(nextJar, targetJar); + }); + } catch (Exception e) { + Files.deleteIfExists(nextJar); + throw e; + } + int exitCode; + try { + exitCode = launchJar(launchJar, appHome, sourceFile, targetJar, args); + } finally { + Files.deleteIfExists(nextJar); + } + System.exit(exitCode); + return true; + } + + private static Path codeSource(Class mainClass) { + URI location; + try { + location = mainClass.getProtectionDomain().getCodeSource().getLocation().toURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + if (!location.getScheme().equals("file")) { + throw new RuntimeException(String.format("Cannot determine classpath for wrapper Jar from codebase '%s'.", location)); + } + try { + return Paths.get(location); + } catch (NoClassDefFoundError e) { + return new File(location.getPath()).toPath(); + } + } + + private static Path requireAbsolutePath(String name) { + String value = System.getProperty(name); + if (value == null || value.isEmpty()) { + throw new RuntimeException("Missing required system property: " + name); + } + Path path = Paths.get(value); + if (!path.isAbsolute()) { + throw new RuntimeException("System property " + name + " must be an absolute path: " + value); + } + return path.normalize(); + } + + /** + * Returns the project directory containing the Wrapper configuration. + * + * @return the normalized absolute application home + */ + public static Path appHome() { + return requireAbsolutePath(APP_HOME_PROPERTY); + } + + static Path sourceFile() { + return requireAbsolutePath(SOURCE_FILE_PROPERTY); + } + + static Path jarFile() { + return requireAbsolutePath(JAR_FILE_PROPERTY); + } + + private static Path classesDir(Path targetJar) { + return targetJar.getParent().resolve(CLASSES_DIR_NAME); + } + + private static T withLock(Path cacheDirectory, LockedAction action) throws Exception { + Files.createDirectories(cacheDirectory); + try (FileChannel channel = FileChannel.open(cacheDirectory.resolve(LOCK_FILE_NAME), StandardOpenOption.CREATE, StandardOpenOption.WRITE); + FileLock ignored = channel.lock()) { + return action.execute(); + } + } + + private static boolean isExpectedJar(Path currentJar, Path targetJar, Path sourceFile) throws Exception { + Path normalizedCurrentJar = currentJar.toAbsolutePath().normalize(); + Path normalizedTargetJar = targetJar.toAbsolutePath().normalize(); + if (normalizedCurrentJar.equals(normalizedTargetJar)) { + return true; + } + Path currentParent = normalizedCurrentJar.getParent(); + Path targetParent = normalizedTargetJar.getParent(); + String currentName = normalizedCurrentJar.getFileName().toString(); + return currentParent != null && currentParent.equals(targetParent) && currentName.startsWith(TEMPORARY_JAR_PREFIX) && currentName.endsWith(".jar") && isCurrent(currentJar, sourceFile); + } + + private static boolean isCurrent(Path jarFile, Path sourceFile) throws Exception { + try (JarFile jar = new JarFile(jarFile.toFile())) { + Manifest manifest = jar.getManifest(); + if (manifest == null) { + return false; + } + Attributes attributes = manifest.getMainAttributes(); + return sha256(sourceFile).equals(attributes.getValue(MANIFEST_SOURCE_SHA256)); + } catch (IOException e) { + return false; + } + } + + private static void compileSource(Path sourceFile, Path classesDir) throws Exception { + recreateDirectory(classesDir); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new RuntimeException("Could not compile " + sourceFile + ". A JDK with the system Java compiler is required."); + } + List options = new ArrayList<>(); + if (SourceVersion.latestSupported().compareTo(SourceVersion.RELEASE_8) > 0) { + options.add("--release"); + options.add("8"); + } else { + options.add("-source"); + options.add("8"); + options.add("-target"); + options.add("8"); + } + options.add("-Xlint:-options"); + options.add("-encoding"); + options.add("UTF-8"); + options.add("-d"); + options.add(classesDir.toString()); + options.add(sourceFile.toString()); + int result = compiler.run(null, null, null, options.toArray(new String[0])); + if (result != 0) { + throw new RuntimeException("Could not compile " + sourceFile + " with the system Java compiler."); + } + } + + private static void recreateDirectory(Path directory) throws IOException { + deleteRecursively(directory); + Files.createDirectories(directory); + } + + private static void writeJar(Path sourceFile, Path classesDir, Path targetJar) throws Exception { + Files.createDirectories(targetJar.getParent()); + Path tempJar = Files.createTempFile(targetJar.getParent(), targetJar.getFileName().toString() + ".", ".tmp"); + try { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attributes.put(Attributes.Name.MAIN_CLASS, MAIN_CLASS_NAME); + attributes.putValue(MANIFEST_SOURCE_SHA256, sha256(sourceFile)); + try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(tempJar), manifest)) { + addClasses(output, classesDir); + } + moveReplacing(tempJar, targetJar); + } finally { + Files.deleteIfExists(tempJar); + } + } + + private static void addClasses(JarOutputStream output, Path classesDir) throws IOException { + Files.walkFileTree(classesDir, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String entryName = classesDir.relativize(file).toString().replace(File.separatorChar, '/'); + output.putNextEntry(new JarEntry(entryName)); + Files.copy(file, output); + output.closeEntry(); + return FileVisitResult.CONTINUE; + } + }); + } + + private static void copyDirectory(Path sourceDirectory, Path targetDirectory) throws IOException { + Files.walkFileTree(sourceDirectory, new SimpleFileVisitor() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Files.createDirectories(targetDirectory.resolve(sourceDirectory.relativize(dir))); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.copy(file, targetDirectory.resolve(sourceDirectory.relativize(file)), StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + }); + } + + private static Path installReplacement(Path sourceJar, Path targetJar) { + try { + moveReplacing(sourceJar, targetJar); + return targetJar; + } catch (IOException e) { + return sourceJar; + } + } + + private static void moveReplacing(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static int launchJar(Path launchJar, Path appHome, Path sourceFile, Path targetJar, String[] args) throws Exception { + List command = new ArrayList<>(); + command.add(javaExecutable()); + command.addAll(forwardedJvmArguments(ManagementFactory.getRuntimeMXBean().getInputArguments(), appHome, sourceFile, targetJar)); + command.add("-jar"); + command.add(launchJar.toString()); + for (String arg : args) { + command.add(arg); + } + return run(command); + } + + static List forwardedJvmArguments(List inputArguments, Path appHome, Path sourceFile, Path jarFile) { + List result = new ArrayList<>(); + for (String inputArgument : inputArguments) { + if (!isSystemPropertyArgument(inputArgument, BOOTSTRAP_PROPERTY) && !inputArgument.startsWith("-Dorg.gradle.wrapper.neo.")) { + result.add(inputArgument); + } + } + result.add("-D" + APP_HOME_PROPERTY + "=" + appHome); + result.add("-D" + SOURCE_FILE_PROPERTY + "=" + sourceFile); + result.add("-D" + JAR_FILE_PROPERTY + "=" + jarFile); + return result; + } + + private static boolean isSystemPropertyArgument(String inputArgument, String property) { + String option = "-D" + property; + return inputArgument.equals(option) || inputArgument.startsWith(option + "="); + } + + private static String javaExecutable() { + Path executable = Paths.get(System.getProperty("java.home"), "bin", isWindows() ? "java.exe" : "java"); + if (Files.isRegularFile(executable)) { + return executable.toString(); + } + return isWindows() ? "java.exe" : "java"; + } + + private static int run(List command) throws Exception { + Process process = new ProcessBuilder(command).inheritIO().start(); + return process.waitFor(); + } + + private static String sha256(Path file) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream input = new DigestInputStream(Files.newInputStream(file), digest)) { + byte[] buffer = new byte[8192]; + while (input.read(buffer) >= 0) { + // Drain the source stream into the digest. + } + } + byte[] hash = digest.digest(); + StringBuilder result = new StringBuilder(hash.length * 2); + for (byte b : hash) { + result.append(Character.forDigit((b >>> 4) & 0xf, 16)); + result.append(Character.forDigit(b & 0xf, 16)); + } + return result.toString(); + } + + private static void deleteRecursively(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + Files.walkFileTree(root, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.deleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.deleteIfExists(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + private static void deleteIfEmpty(Path directory) throws IOException { + if (directory == null || !Files.isDirectory(directory)) { + return; + } + try { + Files.delete(directory); + } catch (IOException ignored) { + // Directory is not empty or cannot be removed; leaving it behind is harmless. + } + } + + private static boolean isWindows() { + return File.separatorChar == '\\'; + } + + @FunctionalInterface + private interface LockedAction { + + T execute() throws Exception; + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 8bdaf60c75ab801e22807dde59e12a8735a34077..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45457 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 c82=K%QXLKFx%@O{wJjEi6Y56o#$)Bpeg diff --git a/gradlew b/gradlew index adff685a034..46f43df81b5 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# Gradle Wrapper Neo startup script for POSIX. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -56,29 +56,29 @@ # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. +# (3) This file is maintained by the Gradle Wrapper Neo project. It compiles +# gradle/wrapper/GradleWrapperNeo.java on demand and launches the Wrapper +# without storing a Wrapper JAR in the project. # -# You can find Gradle at https://github.com/gradle/gradle/. +# Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo # ############################################################################## -# Attempt to set APP_HOME +# Determine the directory containing this launcher # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + LAUNCHER_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; + *) app_path=$LAUNCHER_HOME$link ;; esac done @@ -86,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit +LAUNCHER_HOME=$( cd -P "${LAUNCHER_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -170,7 +170,7 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + LAUNCHER_HOME=$( cygpath --path --mixed "$LAUNCHER_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -202,17 +202,110 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Select the wrapper entry point. GradleWrapperNeo.java is compiled to a temporary +# classes directory only for the first bootstrap; Java packages the final JAR. +LAUNCHER_WRAPPER_DIR=$LAUNCHER_HOME/gradle/wrapper +APP_HOME= + +if [ -f "$LAUNCHER_WRAPPER_DIR/gradle-wrapper.properties" ] ; then + APP_HOME=$LAUNCHER_HOME +else + NEO_SEARCH_START=$( pwd -P ) + NEO_SEARCH_DIR=$NEO_SEARCH_START + while [ -z "$APP_HOME" ] ; do + NEO_CANDIDATE_WRAPPER_DIR=$NEO_SEARCH_DIR/gradle/wrapper + if [ -f "$NEO_CANDIDATE_WRAPPER_DIR/gradle-wrapper.properties" ] ; then + APP_HOME=$NEO_SEARCH_DIR + elif [ "$NEO_SEARCH_DIR" = / ] ; then + die "ERROR: Could not find gradle/wrapper/gradle-wrapper.properties searching from $NEO_SEARCH_START." + else + NEO_SEARCH_DIR=${NEO_SEARCH_DIR%/*} + if [ -z "$NEO_SEARCH_DIR" ] ; then + NEO_SEARCH_DIR=/ + fi + fi + done +fi + +PROJECT_NEO_SOURCE=$APP_HOME/gradle/wrapper/GradleWrapperNeo.java +if [ -f "$PROJECT_NEO_SOURCE" ] ; then + NEO_SOURCE=$PROJECT_NEO_SOURCE +else + NEO_SOURCE=$LAUNCHER_WRAPPER_DIR/GradleWrapperNeo.java +fi + +NEO_WORK_DIR=$APP_HOME/.gradle/wrapper-neo +NEO_JAR=$NEO_WORK_DIR/gradle-wrapper-neo.jar +NEO_BOOTSTRAP_DIR=$NEO_WORK_DIR/bootstrap/$$ +NEO_CLASSES_DIR=$NEO_BOOTSTRAP_DIR/classes + +NEO_JAVA_APP_HOME=$APP_HOME +NEO_JAVA_SOURCE=$NEO_SOURCE +NEO_JAVA_JAR=$NEO_JAR +NEO_JAVA_CLASSES_DIR=$NEO_CLASSES_DIR +if "$cygwin" || "$msys" ; then + NEO_JAVA_APP_HOME=$( cygpath --mixed "$APP_HOME" ) + NEO_JAVA_SOURCE=$( cygpath --mixed "$NEO_SOURCE" ) + NEO_JAVA_JAR=$( cygpath --mixed "$NEO_JAR" ) + NEO_JAVA_CLASSES_DIR=$( cygpath --mixed "$NEO_CLASSES_DIR" ) +fi + +if [ -f "$NEO_SOURCE" ] ; then + if [ -n "$NEO_JAR" ] && [ -f "$NEO_JAR" ] ; then + set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + "-Dorg.gradle.wrapper.neo.app-home=$NEO_JAVA_APP_HOME" \ + "-Dorg.gradle.wrapper.neo.source-file=$NEO_JAVA_SOURCE" \ + "-Dorg.gradle.wrapper.neo.jar-file=$NEO_JAVA_JAR" \ + -jar "$NEO_JAVA_JAR" \ + "$@" + else + if [ -n "$JAVA_HOME" ] ; then + JAVACMD_COMPILER=$JAVA_HOME/bin/javac + else + JAVACMD_COMPILER=javac + fi + if ! command -v "$JAVACMD_COMPILER" >/dev/null 2>&1 + then + die "ERROR: GradleWrapperNeo.java exists, but javac could not be found. + +Please run this wrapper with a JDK so GradleWrapperNeo.java can be compiled." + fi + + rm -rf "$NEO_CLASSES_DIR" + mkdir -p "$NEO_CLASSES_DIR" || exit + JAVAC_VERSION=$("$JAVACMD_COMPILER" -version 2>&1) + case "$JAVAC_VERSION" in + javac\ 1.*) JAVAC_TARGET_ARGS="-source 8 -target 8" ;; + *) JAVAC_TARGET_ARGS="--release 8" ;; + esac + # shellcheck disable=SC2086 + if ! "$JAVACMD_COMPILER" $JAVAC_TARGET_ARGS -Xlint:-options -encoding UTF-8 -d "$NEO_CLASSES_DIR" "$NEO_SOURCE" + then + rm -rf "$NEO_CLASSES_DIR" + die "ERROR: Could not compile $NEO_SOURCE" + fi + + set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + "-Dorg.gradle.wrapper.neo.app-home=$NEO_JAVA_APP_HOME" \ + "-Dorg.gradle.wrapper.neo.source-file=$NEO_JAVA_SOURCE" \ + "-Dorg.gradle.wrapper.neo.jar-file=$NEO_JAVA_JAR" \ + "-Dgradle.wrapper.neo.bootstrap=true" \ + -cp "$NEO_JAVA_CLASSES_DIR" \ + GradleWrapperNeo \ + "$@" + fi +else + die "ERROR: $NEO_SOURCE was not found." +fi + # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then diff --git a/gradlew.bat b/gradlew.bat index c4bdd3ab8e3..2853fe28ca0 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,75 +19,40 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem Gradle Wrapper Neo forwarding script for Windows +@rem +@rem This file is maintained by the Gradle Wrapper Neo project. It delegates +@rem to gradlew.ps1, which compiles GradleWrapperNeo.java on demand and +@rem launches the Wrapper without storing a Wrapper JAR in the project. +@rem +@rem Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" +setlocal EnableExtensions DisableDelayedExpansion -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 +set "POWERSHELL_EXE=pwsh.exe" +"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -Command "exit 0" >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail +set "POWERSHELL_EXE=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" +if exist "%POWERSHELL_EXE%" goto execute -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute +set "POWERSHELL_EXE=powershell.exe" +"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -Command "exit 0" >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo ERROR: PowerShell could not be found. 1>&2 echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo Please install Windows PowerShell 5.1 or PowerShell 7 and make it available in PATH. 1>&2 -goto fail +endlocal +exit /b 1 :execute -@rem Setup the command line - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +set "GRADLE_WRAPPER_NEO_BATCH_LAUNCH=1" +set "GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS=%*" +"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0gradlew.ps1" +set "EXIT_CODE=%ERRORLEVEL%" +endlocal & exit /b %EXIT_CODE% diff --git a/gradlew.ps1 b/gradlew.ps1 new file mode 100644 index 00000000000..6f78ce1c664 --- /dev/null +++ b/gradlew.ps1 @@ -0,0 +1,373 @@ +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle Wrapper Neo startup script for Windows. +# +# This file is maintained by the Gradle Wrapper Neo project. It compiles +# gradle/wrapper/GradleWrapperNeo.java on demand and launches the Wrapper +# without storing a Wrapper JAR in the project. +# +# Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo +# +############################################################################## + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' +$script:WrapperExitCode = 1 + +$script:AppBaseName = [IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name) +$script:LauncherHome = [IO.Path]::GetFullPath($PSScriptRoot) + +function ConvertFrom-WindowsCommandLine { + param([string] $CommandLine) + + if ($null -eq ('GradleWrapperNeo.NativeCommandLine' -as [type])) { + Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; + +namespace GradleWrapperNeo { + public static class NativeCommandLine { + [DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr CommandLineToArgvW(string commandLine, out int argumentCount); + + [DllImport("kernel32.dll")] + public static extern IntPtr LocalFree(IntPtr memory); + } +} +"@ + } + + $nativeCommandLine = 'gradlew.exe ' + $CommandLine + $argumentCount = 0 + $argumentVector = [GradleWrapperNeo.NativeCommandLine]::CommandLineToArgvW( + $nativeCommandLine, + [ref] $argumentCount + ) + if ($argumentVector -eq [IntPtr]::Zero) { + throw [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error()) + } + + try { + $result = New-Object 'System.Collections.Generic.List[string]' + for ($index = 1; $index -lt $argumentCount; $index++) { + $argumentPointer = [Runtime.InteropServices.Marshal]::ReadIntPtr( + $argumentVector, + $index * [IntPtr]::Size + ) + [void] $result.Add([Runtime.InteropServices.Marshal]::PtrToStringUni($argumentPointer)) + } + return $result.ToArray() + } finally { + [void] [GradleWrapperNeo.NativeCommandLine]::LocalFree($argumentVector) + } +} + +function Split-ArgumentString { + param([string] $Value) + + if ($null -eq $Value) { + return @() + } + + $result = New-Object 'System.Collections.Generic.List[string]' + $currentOption = New-Object Text.StringBuilder + [char] $currentQuote = [char] 0 + $insideQuote = $false + $hasOption = $false + + for ($index = 0; $index -lt $Value.Length; $index++) { + [char] $character = $Value[$index] + if ((-not $insideQuote) -and [char]::IsWhiteSpace($character)) { + if ($hasOption) { + [void] $result.Add($currentOption.ToString()) + [void] $currentOption.Clear() + $hasOption = $false + } + } elseif ((-not $insideQuote) -and (($character -eq [char] 34) -or ($character -eq [char] 39))) { + $currentQuote = $character + $insideQuote = $true + $hasOption = $true + } elseif ($insideQuote -and ($character -eq $currentQuote)) { + $insideQuote = $false + } else { + [void] $currentOption.Append($character) + $hasOption = $true + } + } + + if ($hasOption) { + [void] $result.Add($currentOption.ToString()) + } + + return $result.ToArray() +} + +function Resolve-JavaExecutable { + if (-not [string]::IsNullOrWhiteSpace($env:JAVA_HOME)) { + $javaHome = $env:JAVA_HOME.Trim([char] 34) + $javaExecutable = Join-Path $javaHome 'bin\java.exe' + if (-not (Test-Path -LiteralPath $javaExecutable -PathType Leaf)) { + throw "JAVA_HOME is set to an invalid directory: $javaHome" + } + return [IO.Path]::GetFullPath($javaExecutable) + } + + $javaCommand = Get-Command -Name 'java.exe' -CommandType Application -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($null -eq $javaCommand) { + throw "JAVA_HOME is not set and no 'java' command could be found in PATH." + } + return $javaCommand.Path +} + +function Resolve-JavacExecutable { + if (-not [string]::IsNullOrWhiteSpace($env:JAVA_HOME)) { + $javaHome = $env:JAVA_HOME.Trim([char] 34) + $javacExecutable = Join-Path $javaHome 'bin\javac.exe' + if (Test-Path -LiteralPath $javacExecutable -PathType Leaf) { + return [IO.Path]::GetFullPath($javacExecutable) + } + } else { + $javacCommand = Get-Command -Name 'javac.exe' -CommandType Application -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($null -ne $javacCommand) { + return $javacCommand.Path + } + } + + throw 'GradleWrapperNeo.java exists, but javac could not be found. Run this wrapper with a JDK so the source can be compiled.' +} + +function Find-AppHome { + param([string] $LauncherHome) + + $launcherProperties = Join-Path $LauncherHome 'gradle\wrapper\gradle-wrapper.properties' + if (Test-Path -LiteralPath $launcherProperties -PathType Leaf) { + return $LauncherHome + } + + $currentLocation = Get-Location + if ($currentLocation.Provider.Name -ne 'FileSystem') { + throw "The current location is not a file system directory: $currentLocation" + } + + $searchDirectory = Get-Item -LiteralPath $currentLocation.Path + $searchStart = $searchDirectory.FullName + while ($null -ne $searchDirectory) { + $propertiesFile = Join-Path $searchDirectory.FullName 'gradle\wrapper\gradle-wrapper.properties' + if (Test-Path -LiteralPath $propertiesFile -PathType Leaf) { + return $searchDirectory.FullName + } + $searchDirectory = $searchDirectory.Parent + } + + throw "Could not find gradle/wrapper/gradle-wrapper.properties searching from '$searchStart'." +} + +function ConvertTo-JavaPath { + param([string] $Path) + + return $Path.Replace('\', '/') +} + +function ConvertTo-WindowsCommandLineArgument { + param([AllowEmptyString()][string] $Argument) + + if (($Argument.Length -gt 0) -and ($Argument -notmatch '[\s"]')) { + return $Argument + } + + $result = New-Object Text.StringBuilder + [void] $result.Append([char] 34) + $backslashCount = 0 + for ($index = 0; $index -lt $Argument.Length; $index++) { + [char] $character = $Argument[$index] + if ($character -eq [char] 92) { + $backslashCount++ + continue + } + + if ($character -eq [char] 34) { + [void] $result.Append(([string] [char] 92) * (($backslashCount * 2) + 1)) + } elseif ($backslashCount -gt 0) { + [void] $result.Append(([string] [char] 92) * $backslashCount) + } + $backslashCount = 0 + [void] $result.Append($character) + } + + if ($backslashCount -gt 0) { + [void] $result.Append(([string] [char] 92) * ($backslashCount * 2)) + } + [void] $result.Append([char] 34) + return $result.ToString() +} + +function Invoke-NativeApplication { + param( + [string] $Executable, + [string[]] $Arguments + ) + + $quotedArguments = New-Object 'System.Collections.Generic.List[string]' + foreach ($argument in $Arguments) { + [void] $quotedArguments.Add((ConvertTo-WindowsCommandLineArgument -Argument $argument)) + } + + $startInfo = New-Object Diagnostics.ProcessStartInfo + $startInfo.FileName = $Executable + $startInfo.Arguments = [string]::Join(' ', $quotedArguments.ToArray()) + $startInfo.UseShellExecute = $false + + $process = New-Object Diagnostics.Process + $process.StartInfo = $startInfo + try { + if (-not $process.Start()) { + throw "Could not start '$Executable'." + } + $process.WaitForExit() + return $process.ExitCode + } finally { + $process.Dispose() + } +} + +function Compile-WrapperSource { + param( + [string] $SourceFile, + [string] $ClassesDirectory, + [string] $BootstrapDirectory + ) + + $javacExecutable = Resolve-JavacExecutable + [void] [IO.Directory]::CreateDirectory($ClassesDirectory) + + $versionOutput = & $javacExecutable -version 2>&1 + $versionExitCode = $LASTEXITCODE + if ($versionExitCode -ne 0) { + Remove-Item -LiteralPath $BootstrapDirectory -Recurse -Force -ErrorAction SilentlyContinue + throw "Could not run javac at '$javacExecutable'." + } + $versionText = ($versionOutput | Out-String).Trim() + + if ($versionText.StartsWith('javac 1.')) { + [string[]] $targetArguments = @('-source', '8', '-target', '8') + } else { + [string[]] $targetArguments = @('--release', '8') + } + + [string[]] $compileArguments = @( + $targetArguments + '-Xlint:-options' + '-encoding' + 'UTF-8' + '-d' + $ClassesDirectory + $SourceFile + ) + + & $javacExecutable @compileArguments + $compileExitCode = $LASTEXITCODE + if ($compileExitCode -ne 0) { + Remove-Item -LiteralPath $BootstrapDirectory -Recurse -Force -ErrorAction SilentlyContinue + throw "Could not compile '$SourceFile'." + } +} + +function Invoke-GradleWrapper { + param([string[]] $GradleArguments) + + $appHome = Find-AppHome -LauncherHome $script:LauncherHome + $launcherWrapperDirectory = Join-Path $script:LauncherHome 'gradle\wrapper' + $projectSource = Join-Path $appHome 'gradle\wrapper\GradleWrapperNeo.java' + if (Test-Path -LiteralPath $projectSource -PathType Leaf) { + $sourceFile = $projectSource + } else { + $sourceFile = Join-Path $launcherWrapperDirectory 'GradleWrapperNeo.java' + } + if (-not (Test-Path -LiteralPath $sourceFile -PathType Leaf)) { + throw "Wrapper source file '$sourceFile' was not found." + } + + $workDirectory = Join-Path $appHome '.gradle\wrapper-neo' + $jarFile = Join-Path $workDirectory 'gradle-wrapper-neo.jar' + $bootstrapDirectory = Join-Path (Join-Path $workDirectory 'bootstrap') ([Guid]::NewGuid().ToString('N')) + $classesDirectory = Join-Path $bootstrapDirectory 'classes' + + $javaExecutable = Resolve-JavaExecutable + [string[]] $jvmArguments = @('-Xmx64m', '-Xms64m') + foreach ($optionSource in @($env:JAVA_OPTS, $env:GRADLE_OPTS)) { + foreach ($option in @(Split-ArgumentString -Value $optionSource)) { + $jvmArguments += $option + } + } + + $javaAppHome = ConvertTo-JavaPath -Path $appHome + $javaSourceFile = ConvertTo-JavaPath -Path $sourceFile + $javaJarFile = ConvertTo-JavaPath -Path $jarFile + $javaClassesDirectory = ConvertTo-JavaPath -Path $classesDirectory + + [string[]] $javaArguments = @( + $jvmArguments + "-Dorg.gradle.appname=$script:AppBaseName" + "-Dorg.gradle.wrapper.neo.app-home=$javaAppHome" + "-Dorg.gradle.wrapper.neo.source-file=$javaSourceFile" + "-Dorg.gradle.wrapper.neo.jar-file=$javaJarFile" + ) + + $cachedJar = Get-Item -LiteralPath $jarFile -ErrorAction SilentlyContinue + if (($null -ne $cachedJar) -and ($cachedJar.Length -eq 0)) { + Remove-Item -LiteralPath $jarFile -Force -ErrorAction SilentlyContinue + } + + if (Test-Path -LiteralPath $jarFile -PathType Leaf) { + $javaArguments += @('-jar', $javaJarFile) + } else { + Compile-WrapperSource -SourceFile $sourceFile -ClassesDirectory $classesDirectory -BootstrapDirectory $bootstrapDirectory + $javaArguments += @( + '-Dgradle.wrapper.neo.bootstrap=true' + '-cp' + $javaClassesDirectory + 'GradleWrapperNeo' + ) + } + + $javaArguments += $GradleArguments + $script:WrapperExitCode = Invoke-NativeApplication -Executable $javaExecutable -Arguments $javaArguments +} + +try { + if ($env:GRADLE_WRAPPER_NEO_BATCH_LAUNCH -eq '1') { + $rawArguments = $env:GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS + Remove-Item Env:GRADLE_WRAPPER_NEO_BATCH_LAUNCH -ErrorAction SilentlyContinue + Remove-Item Env:GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS -ErrorAction SilentlyContinue + [string[]] $gradleArguments = @(ConvertFrom-WindowsCommandLine -CommandLine $rawArguments) + } else { + [string[]] $gradleArguments = @($args) + } + Invoke-GradleWrapper -GradleArguments $gradleArguments + exit $script:WrapperExitCode +} catch { + [Console]::Error.WriteLine() + [Console]::Error.WriteLine('ERROR: ' + $_.Exception.Message) + [Console]::Error.WriteLine() + exit 1 +} From 511dd2ec2c8cf8cd8c76e797aa5bf170a1780714 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 13 Jul 2026 02:33:51 +0800 Subject: [PATCH 2/2] feat: Integrate Gradle Wrapper Neo and update properties for improved performance --- build.gradle.kts | 1 + gradle/wrapper/gradle-wrapper.properties | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 45e9058d1ac..f9dc16edf5e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ import org.jackhuang.hmcl.gradle.l10n.ParseLanguageSubtagRegistry plugins { id("checkstyle") + id("org.glavo.gradle-wrapper-neo") version "0.1.0" } group = "org.jackhuang" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3c4f0c2cdee..709cead45eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip -networkTimeout=120000 -validateDistributionUrl=true +distributionSha256Sum=60ea723356d81263e8002fec0fcf9e2b0eee0c0850c7a3d7ab0a63f2ccc601f3 +networkTimeout=10000 +retries=0 +retryBackOffMs=500 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists