/* * Gradle Wrapper Neo single-file source distribution. * * This source file is part of Gradle Wrapper Neo 0.2.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; } }