diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d70c06398c1..4ae8f88b8dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,10 @@ jobs: - name: Clean, build and javadoc run: ./gradlew clean build javadoc -Plog-tests --stacktrace + - name: Allow long file names in git for windows + if: matrix.os == 'windows-latest' + run: git config --system core.longpaths true + - name: CLI integration tests if: matrix.java >= 17 run: ./gradlew :smithy-cli:integ -Plog-tests --stacktrace diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java index 24c7525b6a5..29fb1da6e62 100644 --- a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java +++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java @@ -4,18 +4,25 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasLength; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; + import org.junit.jupiter.api.Test; +import software.amazon.smithy.utils.IoUtils; import software.amazon.smithy.utils.ListUtils; + public class CleanCommandTest { + private static final String PROJECT_NAME = "simple-config-sources"; @Test public void exitNormallyIfBuildDirMissing() { - IntegUtils.run("simple-config-sources", ListUtils.of("clean"), result -> { + IntegUtils.run(PROJECT_NAME, ListUtils.of("clean"), result -> { assertThat(result.getExitCode(), equalTo(0)); assertThat(result.getOutput(), hasLength(0)); }); @@ -23,7 +30,7 @@ public void exitNormallyIfBuildDirMissing() { @Test public void deletesContentsOfBuildDir() { - IntegUtils.withProject("simple-config-sources", root -> { + IntegUtils.withProject(PROJECT_NAME, root -> { try { Path created = Files.createDirectories(root.resolve("build").resolve("smithy").resolve("foo")); assertThat(Files.exists(created), is(true)); @@ -35,4 +42,52 @@ public void deletesContentsOfBuildDir() { } }); } + + @Test + public void cleanRemovesAllCacheDirectories() throws IOException { + IntegUtils.clearCacheDirIfExists(); + try { + Files.createDirectories(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH); + assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH) + && Files.isDirectory(IntegUtils.SMITHY_ROOT_CACHE_PATH)); + IntegUtils.run(PROJECT_NAME, ListUtils.of("clean"), result -> { + assertThat(result.getExitCode(), equalTo(0)); + assertThat(result.getOutput(), hasLength(0)); + assertFalse(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH)); + }); + } finally { + IntegUtils.clearCacheDirIfExists(); + } + } + + @Test + public void cleanWithTemplateOptionRemovesOnlyTemplateDir() throws IOException { + IntegUtils.clearCacheDirIfExists(); + try { + Files.createDirectories(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH); + assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH) + && Files.isDirectory(IntegUtils.SMITHY_ROOT_CACHE_PATH)); + + IntegUtils.withProject(PROJECT_NAME, root -> { + Path created = null; + try { + created = Files.createDirectories(root.resolve("build").resolve("smithy").resolve("foo")); + assertThat(Files.exists(created), is(true)); + RunResult result = IntegUtils.run(root, ListUtils.of("clean", "--templates")); + assertThat(Files.exists(created), is(true)); + assertThat(result.getExitCode(), is(0)); + assertTrue(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH)); + assertFalse(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } finally { + if (created != null) { + IoUtils.rmdir(created); + } + } + }); + } finally { + IntegUtils.clearCacheDirIfExists(); + } + } } diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java index a8f77ba9d19..102a97993af 100644 --- a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java +++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java @@ -3,7 +3,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.Test; import software.amazon.smithy.utils.IoUtils; import software.amazon.smithy.utils.ListUtils; @@ -295,6 +300,54 @@ public void badIncludePathFailureExpected() { }); } + @Test + public void cacheCreatedOnFirstCreationOfTemplate() { + IntegUtils.clearCacheDirIfExists(); + IntegUtils.withProject(PROJECT_NAME, root -> { + try { + RunResult resultFirst = IntegUtils.run(root, ListUtils.of("init", "-o", "hello-world")); + assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH) + && Files.isDirectory(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)); + assertThat(resultFirst.getExitCode(), equalTo(0)); + assertThat(resultFirst.getOutput(), + containsString("template repo cloned")); + assertThat(resultFirst.getOutput(), + containsString("Smithy project created in directory: hello-world")); + IoUtils.rmdir(root.resolve("hello-world")); + + RunResult resultSecond = IntegUtils.run(root, ListUtils.of("init", "-o", "hello-world")); + assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH) + && Files.isDirectory(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)); + assertThat(resultSecond.getExitCode(), equalTo(0)); + assertThat(resultSecond.getOutput(), not(containsString("template repo cloned"))); + assertThat(resultSecond.getOutput(), + containsString("Smithy project created in directory: hello-world")); + } finally { + IntegUtils.clearCacheDirIfExists(); + } + }); + } + + @Test + public void noCacheCreatedWhenLocalRepo() { + IntegUtils.clearCacheDirIfExists(); + IntegUtils.withProject(PROJECT_NAME, templatesDir -> { + setupTemplatesDirectory(templatesDir); + + IntegUtils.withTempDir("exitZero", dir -> { + RunResult result = IntegUtils.run( + dir, ListUtils.of("init", "-t", "quickstart-cli", "-u", templatesDir.toString())); + assertThat(result.getOutput(), + containsString("Smithy project created in directory: quickstart-cli")); + assertThat(result.getExitCode(), is(0)); + assertThat(Files.exists(Paths.get(dir.toString(), "quickstart-cli")), is(true)); + + assertFalse(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)); + assertFalse(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH)); + }); + }); + } + private static void run(List args, Path root) { StringBuilder output = new StringBuilder(); int result = IoUtils.runCommand(args, root, output, Collections.emptyMap()); diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java index 22015c527c0..636a2609d65 100644 --- a/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java +++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java @@ -39,7 +39,9 @@ import software.amazon.smithy.utils.MapUtils; public final class IntegUtils { - + public static final Path SMITHY_ROOT_CACHE_PATH = Paths.get(System.getProperty("java.io.tmpdir")) + .resolve("smithy-cache"); + public static final Path SMITHY_TEMPLATE_CACHE_PATH = SMITHY_ROOT_CACHE_PATH.resolve("templates"); private static final Logger LOGGER = Logger.getLogger(IntegUtils.class.getName()); private IntegUtils() {} @@ -146,4 +148,8 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } }); } + + public static void clearCacheDirIfExists() { + IoUtils.rmdir(SMITHY_ROOT_CACHE_PATH); + } } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java index 8873302bdf6..d861e945945 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java @@ -20,8 +20,10 @@ import java.util.logging.Logger; import software.amazon.smithy.build.SmithyBuild; import software.amazon.smithy.build.model.SmithyBuildConfig; +import software.amazon.smithy.cli.ArgumentReceiver; import software.amazon.smithy.cli.Arguments; import software.amazon.smithy.cli.Command; +import software.amazon.smithy.cli.HelpPrinter; import software.amazon.smithy.utils.IoUtils; final class CleanCommand implements Command { @@ -40,20 +42,32 @@ public String getName() { @Override public String getSummary() { - return "Removes Smithy build artifacts."; + return "Removes Smithy build artifacts and caches."; } @Override public int execute(Arguments arguments, Env env) { arguments.addReceiver(new ConfigOptions()); + arguments.addReceiver(new Options()); CommandAction action = HelpActionWrapper.fromCommand(this, parentCommandName, this::run); return action.apply(arguments, env); } private int run(Arguments arguments, Env env) { - ConfigOptions options = arguments.getReceiver(ConfigOptions.class); - SmithyBuildConfig config = options.createSmithyBuildConfig(); + ConfigOptions configOptions = arguments.getReceiver(ConfigOptions.class); + Options options = arguments.getReceiver(Options.class); + + if (options.cleanTemplateCache) { + LOGGER.fine(() -> "Clearing template cache."); + if (CliCache.getTemplateCache().clear()) { + LOGGER.fine(() -> "No template cache found."); + } + return 0; + } + + + SmithyBuildConfig config = configOptions.createSmithyBuildConfig(); Path dir = config.getOutputDirectory() .map(Paths::get) .orElseGet(SmithyBuild::getDefaultOutputDirectory); @@ -62,6 +76,35 @@ private int run(Arguments arguments, Env env) { LOGGER.fine(() -> "Directory does not exist: " + dir); } LOGGER.fine(() -> "Deleted directory " + dir); + + LOGGER.fine(() -> "Clearing all caches."); + if (!CliCache.clearAll()) { + LOGGER.fine(() -> "No caches found."); + } + return 0; } + + + private static final class Options implements ArgumentReceiver { + private Boolean cleanTemplateCache = false; + + @Override + public boolean testOption(String name) { + switch (name) { + case "--templates": + case "-t": + cleanTemplateCache = true; + return true; + default: + return false; + } + } + + @Override + public void registerHelp(HelpPrinter printer) { + printer.param("--templates", "-t", null, + "Clean only the templates cache."); + } + } } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java new file mode 100644 index 00000000000..7d588d1d24b --- /dev/null +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.cli.commands; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import software.amazon.smithy.cli.CliError; +import software.amazon.smithy.utils.IoUtils; + +interface CliCache { + Path DEFAULT_TEMP_DIR = Paths.get(System.getProperty("java.io.tmpdir")); + Path ROOT_CACHE_DIR = DEFAULT_TEMP_DIR.resolve("smithy-cache"); + + static CliCache getTemplateCache() { + return () -> ROOT_CACHE_DIR.resolve("templates"); + } + + Path getPath(); + + default boolean clear() { + return IoUtils.rmdir(getPath()); + } + + default Path get() { + Path cachePath = getPath(); + if (Files.exists(cachePath)) { + return cachePath; + } + // If cache dir does not exist, create it and all required parent directories + try { + return Files.createDirectories(cachePath); + } catch (IOException e) { + throw new CliError("Could not create cache at path: " + cachePath); + } + } + + static boolean clearAll() { + return IoUtils.rmdir(ROOT_CACHE_DIR); + } +} diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java index 79f5961897b..3200734f47a 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java @@ -16,8 +16,8 @@ package software.amazon.smithy.cli.commands; import java.io.IOException; -import java.net.URISyntaxException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -43,6 +43,7 @@ import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.StringUtils; + final class InitCommand implements Command { private static final int LINE_LENGTH = 100; private static final String COLUMN_SEPARATOR = " "; @@ -89,32 +90,99 @@ private int run(Arguments arguments, Env env) { Options options = arguments.getReceiver(Options.class); StandardOptions standardOptions = arguments.getReceiver(StandardOptions.class); - try { - Path root = Paths.get("."); - Path temp = Files.createTempDirectory("temp"); + boolean isLocalRepo = isLocalRepo(options.repositoryUrl); + Path templateRepoDirPath = getTemplateRepoDirPath(options.repositoryUrl, isLocalRepo); + + // If the cache directory does not exist, create it + if (!Files.exists(templateRepoDirPath)) { + try (ProgressTracker t = new ProgressTracker(env, + ProgressStyle.dots("cloning template repo", "template repo cloned"), + standardOptions.quiet() + )) { + Path templateCachePath = CliCache.getTemplateCache().get(); + String relativeTemplateDir = templateCachePath.relativize(templateRepoDirPath).toString(); + // Only clone the latest commit from HEAD. Do not include history + exec(ListUtils.of("git", "clone", "--depth", "1", "--single-branch", + options.repositoryUrl, relativeTemplateDir), templateCachePath); + } + } - loadSmithyTemplateJsonFile(options.repositoryUrl, root, temp); + validateTemplateDir(templateRepoDirPath, options.repositoryUrl); + + // Check for updates and update repo to latest if applicable + if (!isLocalRepo) { + // update remote + exec(ListUtils.of("git", "fetch", "--depth", "1"), templateRepoDirPath); + String response = exec(ListUtils.of("git", "rev-list", "origin..HEAD"), templateRepoDirPath); + // If a change was detected, force the template repo to update + if (!StringUtils.isEmpty(response)) { + try (ProgressTracker t = new ProgressTracker(env, + ProgressStyle.dots("updating template cache", "template repo updated"), + standardOptions.quiet() + )) { + exec(ListUtils.of("git", "reset", "--hard", "origin/main"), templateRepoDirPath); + exec(ListUtils.of("git", "clean", "-dfx"), templateRepoDirPath); + } + } + } - ObjectNode smithyTemplatesNode = getSmithyTemplatesNode(temp); - if (options.listTemplates) { - this.listTemplates(smithyTemplatesNode, env); - } else { - this.cloneTemplate(temp, smithyTemplatesNode, options, standardOptions, env); - } - } catch (IOException | InterruptedException | URISyntaxException e) { - throw new RuntimeException(e); + ObjectNode smithyTemplatesNode = readJsonFileAsNode(templateRepoDirPath.resolve(SMITHY_TEMPLATE_JSON)) + .expectObjectNode(); + if (options.listTemplates) { + this.listTemplates(smithyTemplatesNode, env); + } else { + this.cloneTemplate(templateRepoDirPath, smithyTemplatesNode, options, standardOptions, env); } return 0; } - private void listTemplates(ObjectNode smithyTemplatesNode, Env env) throws IOException { + private void validateTemplateDir(Path templateRepoDirPath, String templateUrl) { + if (!Files.isDirectory(templateRepoDirPath)) { + throw new CliError("Template repository " + templateRepoDirPath + " is not a directory"); + } + Path templateJsonPath = templateRepoDirPath.resolve(SMITHY_TEMPLATE_JSON); + if (!Files.exists(templateJsonPath) && Files.isRegularFile(templateJsonPath)) { + throw new CliError("Template repository " + templateUrl + + " does not contain a valid `smithy-templates.json`."); + } + } + + private Path getTemplateRepoDirPath(String repoPath, boolean isLocalRepo) { + if (isLocalRepo) { + // Just use the local path if the git repo is local + return Paths.get(repoPath); + } else { + return CliCache.getTemplateCache().get().resolve(getCacheDirFromURL(repoPath)); + } + } + + // Remove any trailing .git + // Remove "/" and ".." and ":" characters so a directory can be created with no nesting + private String getCacheDirFromURL(final String repositoryUrl) { + return repositoryUrl.replace(".git", "") + .replace(":", "_") + .replace("/", "_") + .replace(".", "_"); + } + + private void listTemplates(ObjectNode smithyTemplatesNode, Env env) { try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stdout())) { buffer.println(getTemplateList(smithyTemplatesNode, env)); } } + private boolean isLocalRepo(String repoPath) { + try { + Path localPath = Paths.get(repoPath); + return Files.exists(localPath); + } catch (InvalidPathException exc) { + return false; + } + + } + private String getTemplateList(ObjectNode smithyTemplatesNode, Env env) { int maxTemplateLength = 0; Map templates = new TreeMap<>(); @@ -158,6 +226,7 @@ private String getTemplateList(ObjectNode smithyTemplatesNode, Env env) { return builder.toString(); } + private static void writeTemplateBorder(ColorBuffer writer, int maxNameLength, int maxDocLength) { writer.print(pad("", maxNameLength).replace(" ", "─"), ColorTheme.TEMPLATE_LIST_BORDER) .print(COLUMN_SEPARATOR) @@ -169,9 +238,8 @@ private static String wrapDocumentation(String doc, int maxLength, int offset) { return StringUtils.wrap(doc, maxLength, System.lineSeparator() + pad("", offset), false); } - private void cloneTemplate(Path temp, ObjectNode smithyTemplatesNode, Options options, - StandardOptions standardOptions, Env env) - throws IOException, InterruptedException, URISyntaxException { + private void cloneTemplate(Path templateRepoDirPath, ObjectNode smithyTemplatesNode, Options options, + StandardOptions standardOptions, Env env) { String template = options.template; String directory = options.directory; @@ -184,7 +252,7 @@ private void cloneTemplate(Path temp, ObjectNode smithyTemplatesNode, Options op if (directory == null) { directory = template; } - final Path dest = Paths.get(directory); + Path dest = Paths.get(directory); if (Files.exists(dest)) { throw new CliError("Output directory `" + directory + "` already exists."); } @@ -195,32 +263,27 @@ private void cloneTemplate(Path temp, ObjectNode smithyTemplatesNode, Options op "Invalid template `%s`. `%s` provides the following templates:%n%n%s", template, getTemplatesName(smithyTemplatesNode), getTemplateList(smithyTemplatesNode, env))); } - ObjectNode templateNode = templatesNode.expectObjectMember(template).expectObjectNode(); - final String templatePath = getTemplatePath(templateNode, template); + String templatePath = getTemplatePath(templateNode, template); List includedFiles = getIncludedFiles(templateNode); - try (ProgressTracker ignored = new ProgressTracker(env, - ProgressStyle.dots("cloning template", "template cloned"), - standardOptions.quiet() - )) { - // Specify the subdirectory to download - exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", templatePath), temp); - // add any additional files that should be included - for (String includedFile : includedFiles) { - exec(ListUtils.of("git", "sparse-checkout", "add", "--no-cone", includedFile), temp); - } - exec(ListUtils.of("git", "checkout"), temp); + Path stagingPath = createStagingRepo(templateRepoDirPath); + + // Specify the subdirectory to check out + exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", templatePath), stagingPath); + // add any additional files that should be included + for (String includedFile : includedFiles) { + exec(ListUtils.of("git", "sparse-checkout", "add", "--no-cone", includedFile), stagingPath); } + exec(ListUtils.of("git", "checkout"), stagingPath); - if (!Files.exists(temp.resolve(templatePath))) { + if (!Files.exists(stagingPath.resolve(templatePath))) { throw new CliError(String.format("Template path `%s` for template \"%s\" is invalid.", templatePath, template)); } - - IoUtils.copyDir(Paths.get(temp.toString(), templatePath), dest); - copyIncludedFiles(temp.toString(), dest.toString(), includedFiles, template); + IoUtils.copyDir(Paths.get(stagingPath.toString(), templatePath), dest); + copyIncludedFiles(stagingPath.toString(), dest.toString(), includedFiles, template, env); if (!standardOptions.quiet()) { try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stdout())) { @@ -229,13 +292,17 @@ private void cloneTemplate(Path temp, ObjectNode smithyTemplatesNode, Options op } } - private static void loadSmithyTemplateJsonFile(String repositoryUrl, Path root, Path temp) { - exec(ListUtils.of("git", "clone", "--filter=blob:none", "--no-checkout", "--depth", "1", "--sparse", - repositoryUrl, temp.toString()), root); - - exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", SMITHY_TEMPLATE_JSON), temp); + private static Path createStagingRepo(Path repoPath) { + Path temp; + try { + temp = Files.createTempDirectory("temp"); + } catch (IOException exc) { + throw new CliError("Unable to create staging directory for template."); + } + exec(ListUtils.of("git", "clone", "--no-checkout", "--depth", "1", "--sparse", + "file://" + repoPath.toString(), temp.toString()), Paths.get(".")); - exec(ListUtils.of("git", "checkout"), temp); + return temp; } private static ObjectNode getSmithyTemplatesNode(Path jsonFilePath) { @@ -271,7 +338,7 @@ private static List getIncludedFiles(ObjectNode templateNode) { } private static void copyIncludedFiles(String temp, String dest, List includedFiles, - String templateName) throws IOException { + String templateName, Env env) { for (String included : includedFiles) { Path includedPath = Paths.get(temp, included); if (!Files.exists(includedPath)) { @@ -284,7 +351,12 @@ private static void copyIncludedFiles(String temp, String dest, List inc if (Files.isDirectory(includedPath)) { IoUtils.copyDir(includedPath, target); } else if (Files.isRegularFile(includedPath)) { - Files.copy(includedPath, target); + try { + Files.copy(includedPath, target); + } catch (IOException e) { + throw new CliError("Unable to copy included file: " + includedPath + + "to destination directory: " + target); + } } } } diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java index 0439e23f042..1a5107cf421 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java @@ -334,6 +334,9 @@ public static boolean rmdir(Path dir) { Files.walkFileTree(dir, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + // Workaround for Windows systems that set some git packfiles to readonly + file.toFile().setWritable(true); + Files.delete(file); return FileVisitResult.CONTINUE; }