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/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 8bdaf60c75a..00000000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ 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 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 +}