mirror of
https://github.com/HMCL-dev/HMCL
synced 2026-08-19 22:23:08 -04:00
4105 lines
153 KiB
Java
4105 lines
153 KiB
Java
/*
|
|
* 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<String, String> commandLineSystemProperties = converter.convert(options, new HashMap<String, String>());
|
|
Map<String, String> 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<String, String> 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<String, String> projectSystemProperties, Map<String, String> userSystemProperties, Map<String, String> commandLineSystemProperties) {
|
|
Map<String, String> gradleSystemProperties = merge(merge(projectSystemProperties, userSystemProperties), commandLineSystemProperties);
|
|
System.getProperties().putAll(gradleSystemProperties);
|
|
}
|
|
|
|
private static void maybeAddGradleUserHomeSystemProperty(Map<String, String> projectSystemProperties, Map<String, String> commandLineSystemProperties) {
|
|
Map<String, String> 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<String, String> merge(Map<String, String> p1, Map<String, String> p2) {
|
|
// If there are duplicate keys, the values from p2 take precedence.
|
|
Map<String, String> 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<T> implements CommandLineConverter<T> {
|
|
|
|
@Override
|
|
public T convert(Iterable<String> 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<Map<String, String>> {
|
|
|
|
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<String, String> convert(ParsedCommandLine options, Map<String, String> 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> {
|
|
|
|
T convert(Iterable<String> 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<String> options = new HashSet<String>();
|
|
|
|
private Class<?> argumentType = Void.TYPE;
|
|
|
|
private String /* // @Nullable */ description;
|
|
|
|
private boolean incubating;
|
|
|
|
private final Set<CommandLineOption> groupWith = new HashSet<CommandLineOption>();
|
|
|
|
private boolean deprecated;
|
|
|
|
private OptionCategory category = OptionCategory.OTHER;
|
|
|
|
public CommandLineOption(Iterable<String> options) {
|
|
for (String option : options) {
|
|
this.options.add(option);
|
|
}
|
|
}
|
|
|
|
public Set<String> 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<CommandLineOption> getGroupWith() {
|
|
return groupWith;
|
|
}
|
|
|
|
void groupWith(Set<CommandLineOption> 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;
|
|
|
|
/**
|
|
* <p>A command-line parser which supports a command/sub-command style command-line interface. Supports the following
|
|
* syntax:</p>
|
|
* <pre>
|
|
* <option>* (<sub-command> <sub-command-option>*)*
|
|
* </pre>
|
|
*
|
|
* <ul> <li>Short options are a '-' followed by a single character. For example: {@code -a}.</li>
|
|
*
|
|
* <li>Long options are '--' followed by multiple characters. For example: {@code --long-option}.</li>
|
|
*
|
|
* <li>Options can take arguments. The argument follows the option. For example: {@code -a arg} or {@code --long
|
|
* arg}.</li>
|
|
*
|
|
* <li>Arguments can be attached to the option using '='. For example: {@code -a=arg} or {@code --long=arg}.</li>
|
|
*
|
|
* <li>Arguments can be attached to short options. For example: {@code -aarg}.</li>
|
|
*
|
|
* <li>Short options can be combined. For example {@code -ab} is equivalent to {@code -a -b}.</li>
|
|
*
|
|
* <li>Anything else is treated as an extra argument. This includes a single {@code -} character.</li>
|
|
*
|
|
* <li>'--' indicates the end of the options. Anything following is not parsed and is treated as extra arguments.</li>
|
|
*
|
|
* <li>The parser is forgiving, and allows '--' to be used with short options and '-' to be used with long
|
|
* options.</li>
|
|
*
|
|
* <li>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.</li>
|
|
*
|
|
* </ul>
|
|
*/
|
|
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<String, CommandLineOption> optionsByString = new HashMap<String, CommandLineOption>();
|
|
|
|
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<String> commandLine) throws CommandLineArgumentException {
|
|
ParsedCommandLine parsedCommandLine = new ParsedCommandLine(new HashSet<CommandLineOption>(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<CommandLineOption> commandLineOptions = new HashSet<CommandLineOption>();
|
|
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<CommandLineOption> commandLineOptions = new TreeSet<>(new OptionComparator());
|
|
commandLineOptions.addAll(optionsByString.values());
|
|
// map options to their categories
|
|
Map<OptionCategory, List<RenderedCommandLineOption>> 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<RenderedCommandLineOption> 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<String> 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<String> splitToLength(String input, int n) {
|
|
List<String> 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<String> optionStrings = new TreeSet<>(new OptionStringComparator());
|
|
optionStrings.addAll(option.getOptions());
|
|
List<String> 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<String> values = new ArrayList<String>();
|
|
|
|
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<CommandLineOption> {
|
|
|
|
@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<String> {
|
|
|
|
@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<String> {
|
|
|
|
@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<String, ParsedCommandLineOption> optionsByString = new HashMap<String, ParsedCommandLineOption>();
|
|
|
|
private final Set<String> presentOptions = new HashSet<String>();
|
|
|
|
private final Set<String> removedOptions = new HashSet<String>();
|
|
|
|
private final List<String> extraArguments = new ArrayList<String>();
|
|
|
|
ParsedCommandLine(Iterable<CommandLineOption> 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<String> 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<String> 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<String> 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<String> values = new ArrayList<String>();
|
|
|
|
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<String> 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.
|
|
*
|
|
* <b>IMPLEMENTATION NOTE</b>
|
|
* We do it this way instead of the way recommended in <a href="https://snyk.io/research/zip-slip-vulnerability"></a>
|
|
* 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<String> 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<String> 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<String> 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> T access(File exclusiveFile, Callable<T> 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<String> split(String arguments) {
|
|
List<String> commandLineArguments = new ArrayList<String>();
|
|
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<? super String, ? extends /* // @Nullable */ String> 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<? super String, ? extends /* // @Nullable */ String> 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<? super String, ? extends /* // @Nullable */ String> 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<String, String> mapEntry(String key, String value) {
|
|
return new AbstractMap.SimpleImmutableEntry<>(key, value);
|
|
}
|
|
|
|
public Map./* // @Nullable */ Entry<String, String> 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<String, String> combined = usernameAndPassword();
|
|
return combined != null ? combined.getKey() : null;
|
|
}
|
|
|
|
public String authorizationTypeDisplayName() {
|
|
return token != null ? "Bearer Token" : "Basic";
|
|
}
|
|
|
|
public Map.Entry<String, String> 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 ? "<TOKEN>" : "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.
|
|
* <p>
|
|
* 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<String, String> 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<String, String> convertSystemProperties(Properties properties) {
|
|
Map<String, String> result = new HashMap<String, String>();
|
|
for (Map.Entry<Object, Object> 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<String, String> systemProperties) {
|
|
this(logger, progressListener, appName, appVersion, systemProperties, DEFAULT_NETWORK_TIMEOUT_MILLISECONDS);
|
|
}
|
|
|
|
public Download(Logger logger, DownloadProgressListener progressListener, String appName, String appVersion, Map<String, String> 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<String, String> 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<String, String> systemProperties;
|
|
|
|
private ProxyAuthenticator(Map<String, String> 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<URI> 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<File> 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<File> 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<File> listDirs(File distDir) {
|
|
if (!distDir.exists()) {
|
|
return emptyList();
|
|
}
|
|
File[] files = distDir.listFiles();
|
|
if (files == null) {
|
|
return emptyList();
|
|
}
|
|
List<File> 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<? extends ZipEntry> 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.
|
|
*
|
|
* <p>Objects preserve member order, numbers are represented by {@link BigDecimal}, duplicate
|
|
* object keys are rejected, and nesting is limited to protect Wrapper startup.</p>
|
|
*/
|
|
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<String, Object> parseObject(int depth) {
|
|
offset++;
|
|
skipWhitespace();
|
|
Map<String, Object> 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<Object> parseArray(int depth) {
|
|
offset++;
|
|
skipWhitespace();
|
|
List<Object> 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.
|
|
*
|
|
* <p>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.</p>
|
|
*/
|
|
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<String> ROOT_FIELDS = fields("version", "mirrors");
|
|
|
|
private static final Set<String> MIRROR_FIELDS = fields("enabled", "pattern", "replacement", "requireChecksum");
|
|
|
|
private final List<Mirror> mirrors;
|
|
|
|
private MirrorConfiguration(List<Mirror> 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<String, Object> 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<Mirror> mirrors = new ArrayList<>();
|
|
if (root.containsKey("mirrors")) {
|
|
Object configuredMirrors = root.get("mirrors");
|
|
List<Object> 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<URI> resolve(URI source, boolean checksumProvided) {
|
|
LinkedHashSet<URI> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> object(Object value, String path) {
|
|
if (!(value instanceof Map)) {
|
|
throw new IllegalArgumentException(path + " must be an object.");
|
|
}
|
|
return (Map<String, Object>) value;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private static List<Object> array(Object value, String path) {
|
|
if (!(value instanceof List)) {
|
|
throw new IllegalArgumentException(path + " must be an array.");
|
|
}
|
|
return (List<Object>) 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<String, Object> object, Set<String> allowedFields, String path) {
|
|
for (String field : object.keySet()) {
|
|
if (!allowedFields.contains(field)) {
|
|
throw new IllegalArgumentException("Unknown field " + path + "." + field + ".");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Set<String> fields(String... values) {
|
|
Set<String> 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}.
|
|
* <p>
|
|
* The algorithm in use by this method is as follows:
|
|
* <ol>
|
|
* <li>Compute the MD5 value of the UTF-8 {@code string}.</li>
|
|
* <li>Truncate leading zeros (i.e., treat the MD5 value as a number).</li>
|
|
* <li>Convert to base 36 (the characters {@code 0-9a-z}).</li>
|
|
* </ol>
|
|
*/
|
|
@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<String, String> getSystemProperties(File propertiesFile) {
|
|
if (!propertiesFile.isFile()) {
|
|
return Collections.emptyMap();
|
|
}
|
|
Properties properties = loadProperties(propertiesFile);
|
|
Map<String, String> systemProperties = new HashMap<String, String>();
|
|
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<String> getJvmArgs(File propertiesFile) {
|
|
if (!propertiesFile.isFile()) {
|
|
return Collections.emptyList();
|
|
}
|
|
Properties properties = loadProperties(propertiesFile);
|
|
List<String> jvmArgs = new ArrayList<String>();
|
|
for (Map.Entry<Object, Object> 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<URI> 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.
|
|
*
|
|
* <p>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.</p>
|
|
*/
|
|
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> T withLock(Path cacheDirectory, LockedAction<T> 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<String> 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<Path>() {
|
|
|
|
@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<Path>() {
|
|
|
|
@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<String> 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<String> forwardedJvmArguments(List<String> inputArguments, Path appHome, Path sourceFile, Path jarFile) {
|
|
List<String> 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<String> 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<Path>() {
|
|
|
|
@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> {
|
|
|
|
T execute() throws Exception;
|
|
}
|
|
}
|