├── .gitignore ├── META-INF └── native-image │ └── build.buildbuddy │ └── buildbuddy │ ├── native-image.properties │ └── reachability-metadata.json ├── README.md ├── build ├── Main.java ├── Manual.java ├── Maven.java ├── Minimal.java ├── Modular.java ├── ModularByMaven.java ├── Modules.java └── buildbuddy ├── dependencies ├── main.properties ├── modules.properties └── test.properties ├── pom.xml ├── sources ├── build │ └── buildbuddy │ │ ├── BuildExecutor.java │ │ ├── BuildExecutorCallback.java │ │ ├── BuildExecutorException.java │ │ ├── BuildExecutorModule.java │ │ ├── BuildStep.java │ │ ├── BuildStepArgument.java │ │ ├── BuildStepContext.java │ │ ├── BuildStepResult.java │ │ ├── ChecksumStatus.java │ │ ├── HashDigestFunction.java │ │ ├── HashFunction.java │ │ ├── Manual.java │ │ ├── Repository.java │ │ ├── RepositoryItem.java │ │ ├── Resolver.java │ │ ├── SequencedProperties.java │ │ ├── maven │ │ ├── MavenDefaultRepository.java │ │ ├── MavenDefaultVersionNegotiator.java │ │ ├── MavenDependencyKey.java │ │ ├── MavenDependencyName.java │ │ ├── MavenDependencyScope.java │ │ ├── MavenDependencyValue.java │ │ ├── MavenLocalPom.java │ │ ├── MavenPomEmitter.java │ │ ├── MavenPomResolver.java │ │ ├── MavenProject.java │ │ ├── MavenRepository.java │ │ ├── MavenUriParser.java │ │ └── MavenVersionNegotiator.java │ │ ├── module │ │ ├── DownloadModuleUris.java │ │ ├── ModularJarResolver.java │ │ ├── ModularProject.java │ │ ├── ModuleInfo.java │ │ └── ModuleInfoParser.java │ │ ├── project │ │ ├── DependenciesModule.java │ │ ├── JavaModule.java │ │ ├── MultiProject.java │ │ ├── MultiProjectDependencies.java │ │ └── MultiProjectModule.java │ │ └── step │ │ ├── Assign.java │ │ ├── Bind.java │ │ ├── Checksum.java │ │ ├── DependencyTransformingBuildStep.java │ │ ├── Download.java │ │ ├── Group.java │ │ ├── Jar.java │ │ ├── Java.java │ │ ├── Javac.java │ │ ├── ProcessBuildStep.java │ │ ├── ProcessHandler.java │ │ ├── Resolve.java │ │ ├── TestEngine.java │ │ ├── Tests.java │ │ └── Translate.java └── module-info.java └── tests ├── build └── buildbuddy │ └── test │ ├── BuildExecutorCallbackTest.java │ ├── BuildExecutorTest.java │ ├── ChecksumStatusTest.java │ ├── HashDigestFunctionTest.java │ ├── HashFunctionTest.java │ ├── SequencedPropertiesTest.java │ ├── maven │ ├── MavenDefaultRepositoryTest.java │ ├── MavenPomEmitterTest.java │ ├── MavenPomResolverTest.java │ ├── MavenProjectTest.java │ └── MavenUriParserTest.java │ ├── module │ ├── ModularJarResolverTest.java │ ├── ModularProjectTest.java │ └── ModuleInfoParserTest.java │ ├── project │ ├── DependenciesModuleTest.java │ ├── JavaModuleTest.java │ ├── MultiProjectDependenciesTest.java │ └── MultiProjectModuleTest.java │ └── step │ ├── AssignTest.java │ ├── BindTest.java │ ├── ChecksumTest.java │ ├── DownloadTest.java │ ├── GroupTest.java │ ├── JarTest.java │ ├── JavaTest.java │ ├── JavacTest.java │ ├── ResolveTest.java │ ├── TestsTest.java │ └── TranslateTest.java ├── module-info.java └── sample ├── Sample.java └── TestSample.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | cache/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/* 9 | *.iws 10 | *.iml 11 | *.ipr 12 | 13 | ### Eclipse ### 14 | .apt_generated 15 | .classpath 16 | .factorypath 17 | .project 18 | .settings 19 | .springBeans 20 | .sts4-cache 21 | 22 | ### VS Code ### 23 | .vscode/ 24 | 25 | ### Mac OS ### 26 | .DS_Store -------------------------------------------------------------------------------- /META-INF/native-image/build.buildbuddy/buildbuddy/native-image.properties: -------------------------------------------------------------------------------- 1 | -H:ResourceConfigurationFiles=${.}/reachability-metadata.json 2 | -------------------------------------------------------------------------------- /META-INF/native-image/build.buildbuddy/buildbuddy/reachability-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundles": [ 3 | { 4 | "name": "com.sun.tools.javac.resources.compiler", 5 | "locales": [ 6 | "en" 7 | ] 8 | }, 9 | { 10 | "name": "com.sun.tools.javac.resources.javac", 11 | "locales": [ 12 | "en" 13 | ] 14 | }, 15 | { 16 | "name": "sun.tools.jar.resources.jar", 17 | "locales": [ 18 | "en" 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Build Buddy 2 | =========== 3 | 4 | POC for a simple-enough, yet powerful enough build tool that targets Java, and is written and configured in Java, and 5 | that has inherent support for (a) parallel incremental builds, and therewith build reproducibility and (b) supply-chain 6 | security when it comes to downloading external resources. 7 | 8 | As a side goal, the build tool should be storable as source code alongside another project, without a need of explicit 9 | installation. At the same time, it should be possible to compile the build to avoid repeated compilation. Doing so, a 10 | build should be executable by using the JVM only once a copy of a project's source is obtained, by embracing the JVM's 11 | ability to run programs from source files. This avoids storing precompiled binaries in repositories, and allows for the 12 | execution of builds in environments that only have the JVM installed without the deployment of build tool wrappers that 13 | often entail a (cachable) download of the tool. It should be possible to manage updates of these sources easily, and to 14 | add extensions (plugins) to the base implementation alongside. 15 | 16 | The build tool should only rely on the Java standard library and should be launchable using a command such as: 17 | 18 | java build/Main.java 19 | 20 | where `Main` is a user defined class located in the project's build folder, which assembles the build using the 21 | classes of this build tool. This is also demonstrated within this project, where the build tool is the source but 22 | also linked into the build folder as it would be suggested to users of this tool. This would also be possible by 23 | using for example Git Submodules. For IDE-support, a POM is stored alongside, and it should always be possible to 24 | build this project using Maven to debug errors in the project source which is used for building itself. 25 | 26 | By automatically caching results of single build steps, expensive but commonly stable tasks should be cached implicitly. 27 | This avoids the need of, for example, repeated resolution of dependency trees. As the result of such resolution can 28 | be stored in a textual format, dependency resolution could also be checked into a source repository. This allows both 29 | to store checksums of previously resolved files for validation, and stabilizes resolution process which can otherwise 30 | render builds non-deterministic, for example when version ranges are declared in (transitive) dependencies. 31 | 32 | To allow for an effective implementation of such caching, dependency descriptors should not be defined as a part of the 33 | build description, but separately. In the simplest format, it should always be possible to express information in the 34 | Java properties format. Based on this, it is trivial to translate common descriptions into this format. As a 35 | demonstration of this concept, Java module info classes should be offered as a canonical way of defining (build) module 36 | names and dependencies. 37 | 38 | Specific implementations of dependency resolution or repositories should not be hard-coded into the build tool. 39 | There should, for example, not be any hard dependency on Maven concepts, to allow for their substitution. 40 | 41 | The POC is currently missing: 42 | - API to rename inherited identifiers within modules for the runtime of the module. 43 | - Convention object for `MultiProject` to avoid manual construction of identifiers. 44 | - Task for creating POM files from module-info.java. 45 | - Task for javadoc. 46 | - Task for source-jars. 47 | - Task to add GPG signatures of artifacts. 48 | - Task to publish to Maven Central and local Maven repository. 49 | - Extend all build step implementations to support their standard options. 50 | -------------------------------------------------------------------------------- /build/Main.java: -------------------------------------------------------------------------------- 1 | package build; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | 7 | public class Main { 8 | 9 | public static void main(String[] args) throws IOException { 10 | if (Files.exists(Path.of("pom.xml"))) { 11 | Maven.main(args); 12 | } else { 13 | Modular.main(args); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build/Manual.java: -------------------------------------------------------------------------------- 1 | package build; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.Repository; 5 | import build.buildbuddy.Resolver; 6 | import build.buildbuddy.maven.MavenDefaultRepository; 7 | import build.buildbuddy.maven.MavenPomResolver; 8 | import build.buildbuddy.maven.MavenRepository; 9 | import build.buildbuddy.step.*; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.util.Map; 14 | 15 | public class Manual { 16 | 17 | public static void main(String[] args) throws IOException { 18 | MavenRepository mavenRepository = new MavenDefaultRepository(); 19 | Map repositories = Map.of("maven", mavenRepository); 20 | Map resolvers = Map.of("maven", new MavenPomResolver()); 21 | 22 | BuildExecutor root = BuildExecutor.of(Path.of("target")); 23 | root.addSource("deps", Path.of("dependencies")); 24 | 25 | root.addModule("main-deps", (module, _) -> { 26 | module.addStep("properties", Bind.asDependencies("main.properties"), "../deps"); 27 | module.addStep("resolved", new Resolve(repositories, resolvers), "properties"); 28 | module.addStep("artifacts", new Download(repositories), "resolved"); 29 | }, "deps"); 30 | root.addModule("main", (module, _) -> { 31 | module.addSource("sources", Bind.asSources(), Path.of("sources")); 32 | module.addStep("classes", Javac.tool(), "sources", "../main-deps/artifacts"); 33 | module.addStep("artifacts", Jar.tool(), "classes"); 34 | }, "main-deps"); 35 | 36 | root.addModule("test-deps", (module, _) -> { 37 | module.addStep("properties", Bind.asDependencies("test.properties"), "../deps"); 38 | module.addStep("resolved", new Resolve(repositories, resolvers), "properties"); 39 | module.addStep("artifacts", new Download(repositories), "resolved"); 40 | }, "deps"); 41 | root.addModule("test", (module, _) -> { 42 | module.addSource("sources", Bind.asSources(), Path.of("tests")); 43 | module.addStep("classes", Javac.tool(), "sources", "../main/artifacts", "../test-deps/artifacts"); 44 | module.addStep("artifacts", Jar.tool(), "classes", "../test-deps/artifacts"); 45 | module.addStep("tests", new Tests(TestEngine.JUNIT5), "classes", "artifacts", "../main/artifacts", "../test-deps/artifacts"); 46 | }, "test-deps", "main"); 47 | 48 | root.execute(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /build/Maven.java: -------------------------------------------------------------------------------- 1 | package build; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.maven.MavenProject; 5 | import build.buildbuddy.project.JavaModule; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Path; 9 | import java.util.LinkedHashSet; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | public class Maven { 14 | 15 | public static void main(String[] args) throws IOException { 16 | BuildExecutor root = BuildExecutor.of(Path.of("target")); 17 | root.addModule("maven", MavenProject.make(Path.of("."), 18 | "SHA256", 19 | (_, _) -> (buildExecutor, inherited) -> buildExecutor.addModule("java", 20 | new JavaModule().testIfAvailable(), 21 | Stream.concat(Stream.of("../dependencies/artifacts"), inherited.sequencedKeySet().stream() 22 | .filter(identity -> identity.startsWith("../../../"))).collect( 23 | Collectors.toCollection(LinkedHashSet::new))))); 24 | root.execute(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build/Minimal.java: -------------------------------------------------------------------------------- 1 | package build; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.Repository; 5 | import build.buildbuddy.Resolver; 6 | import build.buildbuddy.maven.MavenDefaultRepository; 7 | import build.buildbuddy.maven.MavenPomResolver; 8 | import build.buildbuddy.maven.MavenRepository; 9 | import build.buildbuddy.step.*; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.util.Map; 14 | 15 | public class Minimal { 16 | 17 | public static void main(String[] args) throws IOException { 18 | MavenRepository mavenRepository = new MavenDefaultRepository(); 19 | Map repositories = Map.of("maven", mavenRepository); 20 | Map resolvers = Map.of("maven", new MavenPomResolver()); 21 | 22 | BuildExecutor root = BuildExecutor.of(Path.of("target")); 23 | 24 | root.addSource("sources", Bind.asSources(), Path.of("sources")); 25 | root.addStep("main-javac", Javac.tool(), "sources"); 26 | root.addStep("main-jar", Jar.tool(), "main-javac"); 27 | 28 | root.addSource("test-dependencies", Bind.asDependencies("test.properties"), Path.of("dependencies")); 29 | root.addStep("test-dependencies-resolved", new Resolve(repositories, resolvers), "test-dependencies"); 30 | root.addStep("test-dependencies-downloaded", new Download(repositories), "test-dependencies-resolved"); 31 | 32 | root.addSource("test", Bind.asSources(), Path.of("tests")); 33 | root.addStep("test-javac", Javac.tool(), "main-jar", "test-dependencies-downloaded", "test"); 34 | root.addStep("tests", new Tests(TestEngine.JUNIT5).jarsOnly(false), "main-jar", "test-dependencies-downloaded", "test-javac"); 35 | 36 | root.execute(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /build/Modular.java: -------------------------------------------------------------------------------- 1 | package build; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.Repository; 5 | import build.buildbuddy.module.DownloadModuleUris; 6 | import build.buildbuddy.module.ModularProject; 7 | import build.buildbuddy.project.JavaModule; 8 | 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.LinkedHashSet; 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.Stream; 17 | 18 | public class Modular { 19 | 20 | public static void main(String[] args) throws IOException { 21 | BuildExecutor root = BuildExecutor.of(Path.of("target")); 22 | root.addStep("download", new DownloadModuleUris("module", List.of( 23 | DownloadModuleUris.DEFAULT, 24 | Path.of("dependencies/modules.properties").toUri()))); 25 | root.addModule("build", (build, downloaded) -> build.addModule("modules", ModularProject.make( 26 | Path.of("."), 27 | "SHA256", 28 | Repository.ofProperties(DownloadModuleUris.URIS, 29 | downloaded.values(), 30 | URI::create, 31 | Files.createDirectories(Path.of("cache/modules"))), 32 | (_, _) -> (buildExecutor, inherited) -> buildExecutor.addModule("java", 33 | new JavaModule().testIfAvailable(), 34 | Stream.concat(Stream.of("../dependencies/artifacts"), inherited.sequencedKeySet().stream() 35 | .filter(identity -> identity.startsWith("../../../"))).collect( 36 | Collectors.toCollection(LinkedHashSet::new))))), "download"); 37 | root.execute(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /build/ModularByMaven.java: -------------------------------------------------------------------------------- 1 | package build; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.Repository; 5 | import build.buildbuddy.Resolver; 6 | import build.buildbuddy.maven.MavenDefaultRepository; 7 | import build.buildbuddy.maven.MavenPomResolver; 8 | import build.buildbuddy.maven.MavenUriParser; 9 | import build.buildbuddy.module.DownloadModuleUris; 10 | import build.buildbuddy.module.ModularJarResolver; 11 | import build.buildbuddy.module.ModularProject; 12 | import build.buildbuddy.project.JavaModule; 13 | import build.buildbuddy.step.Resolve; 14 | 15 | import java.io.IOException; 16 | import java.nio.file.Path; 17 | import java.util.LinkedHashSet; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Optional; 21 | import java.util.function.Function; 22 | import java.util.stream.Collectors; 23 | import java.util.stream.Stream; 24 | 25 | public class ModularByMaven { 26 | 27 | public static void main(String[] args) throws IOException { 28 | Map repositories = Map.of("maven", new MavenDefaultRepository()); 29 | 30 | BuildExecutor root = BuildExecutor.of(Path.of("target")); 31 | root.addStep("download", new DownloadModuleUris(null, List.of( 32 | DownloadModuleUris.DEFAULT, 33 | Path.of("dependencies/modules.properties").toUri()))); 34 | root.addModule("build", (build, downloaded) -> { 35 | Function parser = MavenUriParser.ofUris(new MavenUriParser(), 36 | DownloadModuleUris.URIS, 37 | downloaded.values()); 38 | build.addModule("modules", ModularProject.make( 39 | Path.of("."), 40 | "SHA256", 41 | repositories, 42 | Map.of("module", new ModularJarResolver( 43 | false, 44 | new MavenPomResolver().translated("maven", (_, coordinate) -> parser.apply(coordinate)))), 45 | (_, _) -> (buildExecutor, inherited) -> buildExecutor.addModule("java", 46 | new JavaModule().testIfAvailable(), 47 | Stream.concat(Stream.of("../dependencies/artifacts"), inherited.sequencedKeySet().stream() 48 | .filter(identity -> identity.startsWith("../../../"))).collect( 49 | Collectors.toCollection(LinkedHashSet::new))))); 50 | }, "download"); 51 | root.execute(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /build/Modules.java: -------------------------------------------------------------------------------- 1 | package build; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.Repository; 5 | import build.buildbuddy.Resolver; 6 | import build.buildbuddy.maven.MavenDefaultRepository; 7 | import build.buildbuddy.maven.MavenPomResolver; 8 | import build.buildbuddy.maven.MavenRepository; 9 | import build.buildbuddy.project.DependenciesModule; 10 | import build.buildbuddy.project.JavaModule; 11 | import build.buildbuddy.step.Bind; 12 | import build.buildbuddy.step.TestEngine; 13 | 14 | import java.io.IOException; 15 | import java.nio.file.Path; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | 19 | public class Modules { 20 | 21 | public static void main(String[] args) throws IOException { 22 | MavenRepository mavenRepository = new MavenDefaultRepository(); 23 | Map repositories = Map.of("maven", mavenRepository); 24 | Map resolvers = Map.of("maven", new MavenPomResolver()); 25 | 26 | BuildExecutor root = BuildExecutor.of(Path.of("target")); 27 | root.addSource("deps", Path.of("dependencies")); 28 | 29 | root.addStep("main-deps", Bind.asDependencies("main.properties"), "deps"); 30 | root.addModule("main-artifacts", new DependenciesModule(repositories, resolvers), "main-deps"); 31 | root.addSource("main-sources", Bind.asSources(), Path.of("sources")); 32 | root.addModule("main", new JavaModule(), identifier -> identifier.equals("artifacts") ? Optional.of(identifier) : Optional.empty(), "main-artifacts", "main-sources"); 33 | 34 | root.addStep("test-deps", Bind.asDependencies("test.properties"), "deps"); 35 | root.addModule("test-artifacts", new DependenciesModule(repositories, resolvers), "test-deps"); 36 | root.addSource("test-sources", Bind.asSources(), Path.of("tests")); 37 | root.addModule("test", new JavaModule().test(TestEngine.JUNIT5), "test-artifacts", "test-sources", "main"); 38 | 39 | root.execute(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /build/buildbuddy: -------------------------------------------------------------------------------- 1 | ../sources/build/buildbuddy -------------------------------------------------------------------------------- /dependencies/main.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphw/build-buddy/5ab1dee43f2c543550bd94c484d82485976412d0/dependencies/main.properties -------------------------------------------------------------------------------- /dependencies/modules.properties: -------------------------------------------------------------------------------- 1 | org.opentest4j.reporting.tooling.spi=https://repo.maven.apache.org/maven2/org/opentest4j/reporting/open-test-reporting-tooling-spi/0.2.0-M2/open-test-reporting-tooling-spi-0.2.0-M2.jar 2 | -------------------------------------------------------------------------------- /dependencies/test.properties: -------------------------------------------------------------------------------- 1 | maven/org.junit.jupiter/junit-jupiter/5.11.3 2 | maven/org.junit.platform/junit-platform-console/1.11.4 3 | maven/org.assertj/assertj-core/3.27.0 4 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | codes.rafael.buildbuddy 8 | build-tool 9 | 1.0-SNAPSHOT 10 | 11 | 12 | sources 13 | tests 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-resources-plugin 21 | 22 | true 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 24 31 | UTF-8 32 | 33 | 34 | 35 | 36 | org.junit.jupiter 37 | junit-jupiter 38 | 5.11.3 39 | test 40 | 41 | 42 | org.junit.platform 43 | junit-platform-console 44 | 1.11.4 45 | test 46 | 47 | 48 | org.assertj 49 | assertj-core 50 | 3.27.0 51 | test 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/BuildExecutorCallback.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.PrintStream; 4 | import java.util.SequencedSet; 5 | import java.util.function.BiConsumer; 6 | 7 | public interface BuildExecutorCallback { 8 | 9 | BiConsumer step(String identity, SequencedSet keys); 10 | 11 | static BuildExecutorCallback nop() { 12 | return (_, _) -> (_, _) -> { 13 | }; 14 | } 15 | 16 | static BuildExecutorCallback printing(PrintStream out) { 17 | return (identity, _) -> { 18 | long started = System.nanoTime(); 19 | if (identity == null) { 20 | out.printf("Starting build...%n"); 21 | return (_, throwable) -> { 22 | double time = ((double) (System.nanoTime() - started) / 1_000_000) / 1_000; 23 | out.printf("%s build in %.2f seconds%n", throwable == null ? "COMPLETED" : "FAILED", time); 24 | }; 25 | } else { 26 | return (executed, throwable) -> { 27 | if (throwable != null) { 28 | out.printf("[FAILED] %s: %s%n", identity, throwable instanceof BuildExecutorException 29 | ? throwable.getCause().getMessage() 30 | : throwable.getMessage()); 31 | } else if (executed) { 32 | double time = ((double) (System.nanoTime() - started) / 1_000_000) / 1_000; 33 | out.printf("[EXECUTED] %s in %.2f seconds%n", identity, time); 34 | } else { 35 | out.printf("[SKIPPED] %s%n", identity); 36 | } 37 | }; 38 | } 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/BuildExecutorException.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.util.concurrent.CompletionException; 4 | 5 | public class BuildExecutorException extends CompletionException { 6 | 7 | public BuildExecutorException(String identity, Throwable cause) { 8 | super("Failed to execute " + identity, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/BuildExecutorModule.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | import java.util.Optional; 6 | import java.util.SequencedMap; 7 | 8 | @FunctionalInterface 9 | public interface BuildExecutorModule { 10 | 11 | String PREVIOUS = "../"; 12 | 13 | default Optional resolve(String path) { 14 | return Optional.of(path); 15 | } 16 | 17 | void accept(BuildExecutor buildExecutor, SequencedMap inherited) throws IOException; 18 | } 19 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/BuildStep.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.IOException; 4 | import java.util.SequencedMap; 5 | import java.util.concurrent.CompletionStage; 6 | import java.util.concurrent.Executor; 7 | 8 | @FunctionalInterface 9 | public interface BuildStep { 10 | 11 | String SOURCES = "sources/", RESOURCES = "resources/", CLASSES = "classes/", ARTIFACTS = "artifacts/"; 12 | String COORDINATES = "coordinates.properties", DEPENDENCIES = "dependencies.properties"; 13 | 14 | default boolean shouldRun(SequencedMap arguments) { 15 | return arguments.values().stream().anyMatch(BuildStepArgument::hasChanged); 16 | } 17 | 18 | CompletionStage apply(Executor executor, 19 | BuildStepContext context, 20 | SequencedMap arguments) throws IOException; 21 | } 22 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/BuildStepArgument.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.nio.file.Path; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.Map; 7 | 8 | public record BuildStepArgument(Path folder, Map files) { 9 | 10 | public boolean hasChanged() { 11 | return files.values().stream().anyMatch(status -> status != ChecksumStatus.RETAINED); 12 | } 13 | 14 | public boolean hasChanged(Path... prefixes) { 15 | return hasChanged(Arrays.asList(prefixes)); 16 | } 17 | 18 | public boolean hasChanged(Collection prefixes) { 19 | return files.entrySet().stream() 20 | .filter(entry -> prefixes.stream().anyMatch(prefix -> entry.getKey().startsWith(prefix))) 21 | .anyMatch(entry -> entry.getValue() != ChecksumStatus.RETAINED); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/BuildStepContext.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.nio.file.Path; 4 | 5 | public record BuildStepContext(Path previous, Path next, Path supplement) { 6 | } 7 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/BuildStepResult.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | public record BuildStepResult(boolean next) { 4 | } 5 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/ChecksumStatus.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.nio.file.Path; 4 | import java.util.Arrays; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import java.util.Set; 8 | 9 | public enum ChecksumStatus { 10 | 11 | ADDED, REMOVED, ALTERED, RETAINED; 12 | 13 | public static Map diff(Map expected, Map actual) { 14 | Map diff = new LinkedHashMap<>(); 15 | Map removed = new LinkedHashMap<>(expected); 16 | for (Map.Entry entry : actual.entrySet()) { 17 | byte[] other = removed.remove(entry.getKey()); 18 | if (other == null) { 19 | diff.put(entry.getKey(), ADDED); 20 | } else if (Arrays.equals(other, entry.getValue())) { 21 | diff.put(entry.getKey(), RETAINED); 22 | } else { 23 | diff.put(entry.getKey(), ALTERED); 24 | } 25 | } 26 | for (Path path : removed.keySet()) { 27 | diff.put(path, REMOVED); 28 | } 29 | return diff; 30 | } 31 | 32 | public static Map added(Set paths) { 33 | Map diff = new LinkedHashMap<>(); 34 | paths.forEach(path -> diff.put(path, ADDED)); 35 | return diff; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/HashDigestFunction.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.IOException; 4 | import java.nio.channels.FileChannel; 5 | import java.nio.file.Path; 6 | import java.security.MessageDigest; 7 | import java.security.NoSuchAlgorithmException; 8 | 9 | public class HashDigestFunction implements HashFunction { 10 | 11 | private final String algorithm; 12 | 13 | public HashDigestFunction(String algorithm) { 14 | this.algorithm = algorithm; 15 | } 16 | 17 | @Override 18 | public byte[] hash(Path file) throws IOException { 19 | MessageDigest digest; 20 | try { 21 | digest = MessageDigest.getInstance(algorithm); 22 | } catch (NoSuchAlgorithmException e) { 23 | throw new IllegalStateException(e); 24 | } 25 | try (FileChannel channel = FileChannel.open(file)) { 26 | digest.update(channel.map(FileChannel.MapMode.READ_ONLY, channel.position(), channel.size())); 27 | } 28 | return digest.digest(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/HashFunction.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.IOException; 6 | import java.nio.file.DirectoryStream; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.ArrayDeque; 11 | import java.util.Arrays; 12 | import java.util.HashMap; 13 | import java.util.HexFormat; 14 | import java.util.Iterator; 15 | import java.util.LinkedHashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Queue; 19 | 20 | @FunctionalInterface 21 | public interface HashFunction { 22 | 23 | byte[] hash(Path file) throws IOException; 24 | 25 | static HashFunction ofSize() { 26 | return file -> { 27 | long size = Files.size(file); 28 | byte[] hash = new byte[Long.BYTES]; 29 | for (int index = Long.BYTES - 1; index >= 0; index--) { 30 | hash[index] = (byte) (size & 0xFF); 31 | size >>= Byte.SIZE; 32 | } 33 | return hash; 34 | }; 35 | } 36 | 37 | static HashFunction ofLastModified() { 38 | return file -> { 39 | long lastModified = Files.getLastModifiedTime(file).toMillis(); 40 | byte[] hash = new byte[Long.BYTES]; 41 | for (int index = Long.BYTES - 1; index >= 0; index--) { 42 | hash[index] = (byte) (lastModified & 0xFF); 43 | lastModified >>= Byte.SIZE; 44 | } 45 | return hash; 46 | }; 47 | } 48 | 49 | static Map read(Path file) throws IOException { 50 | Map checksums = new LinkedHashMap<>(); 51 | try (BufferedReader reader = Files.newBufferedReader(file)) { 52 | Iterator it = reader.lines().iterator(); 53 | while (it.hasNext()) { 54 | checksums.put(Paths.get(it.next()), HexFormat.of().parseHex(it.next())); 55 | } 56 | } 57 | return checksums; 58 | } 59 | 60 | static Map read(Path folder, HashFunction hash) throws IOException { 61 | Map checksums = new LinkedHashMap<>(); 62 | Queue queue = new ArrayDeque<>(List.of(folder)); 63 | do { 64 | Path current = queue.remove(); 65 | if (Files.isDirectory(current)) { 66 | try (DirectoryStream stream = Files.newDirectoryStream(current)) { 67 | stream.forEach(queue::add); 68 | } 69 | } else { 70 | checksums.put(folder.relativize(current), hash.hash(current)); 71 | } 72 | } while (!queue.isEmpty()); 73 | return checksums; 74 | } 75 | 76 | static void write(Path file, Map checksums) throws IOException { 77 | try (BufferedWriter writer = Files.newBufferedWriter(file)) { 78 | for (Map.Entry entry : checksums.entrySet()) { 79 | writer.append(entry.getKey().toString()); 80 | writer.newLine(); 81 | writer.append(HexFormat.of().formatHex(entry.getValue())); 82 | writer.newLine(); 83 | } 84 | } 85 | } 86 | 87 | static boolean areConsistent(Path folder, Map checksums, HashFunction hash) throws IOException { 88 | Map remaining = new HashMap<>(checksums); 89 | Queue queue = new ArrayDeque<>(List.of(folder)); 90 | do { 91 | Path current = queue.remove(); 92 | if (Files.isDirectory(current)) { 93 | try (DirectoryStream stream = Files.newDirectoryStream(current)) { 94 | stream.forEach(queue::add); 95 | } 96 | } else { 97 | byte[] checksum = remaining.remove(folder.relativize(current)); 98 | if (checksum == null || !Arrays.equals(checksum, hash.hash(current))) { 99 | return false; 100 | } 101 | } 102 | } while (!queue.isEmpty()); 103 | return remaining.isEmpty(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/Manual.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import build.buildbuddy.maven.MavenDefaultRepository; 4 | import build.buildbuddy.maven.MavenPomResolver; 5 | import build.buildbuddy.maven.MavenRepository; 6 | import build.buildbuddy.step.Bind; 7 | import build.buildbuddy.step.Download; 8 | import build.buildbuddy.step.Jar; 9 | import build.buildbuddy.step.Javac; 10 | import build.buildbuddy.step.Resolve; 11 | import build.buildbuddy.step.Tests; 12 | 13 | import java.io.IOException; 14 | import java.nio.file.Path; 15 | import java.util.Map; 16 | 17 | public class Manual { 18 | 19 | public static void main(String[] args) throws IOException { 20 | MavenRepository mavenRepository = new MavenDefaultRepository(); 21 | Map repositories = Map.of("maven", mavenRepository); 22 | Map resolvers = Map.of("maven", new MavenPomResolver()); 23 | 24 | BuildExecutor root = BuildExecutor.of(Path.of("target")); 25 | root.addSource("deps", Path.of("dependencies")); 26 | 27 | root.addModule("main-deps", (module, _) -> { 28 | module.addStep("properties", Bind.asDependencies("main.properties"), "../deps"); 29 | module.addStep("resolved", new Resolve(repositories, resolvers), "properties"); 30 | module.addStep("artifacts", new Download(repositories), "resolved"); 31 | }, "deps"); 32 | root.addModule("main", (module, _) -> { 33 | module.addSource("sources", Bind.asSources(), Path.of("sources")); 34 | module.addStep("classes", Javac.tool(), "sources", "../main-deps/artifacts"); 35 | module.addStep("artifacts", Jar.tool(), "classes"); 36 | }, "main-deps"); 37 | 38 | root.addModule("test-deps", (module, _) -> { 39 | module.addStep("properties", Bind.asDependencies("test.properties"), "../deps"); 40 | module.addStep("resolved", new Resolve(repositories, resolvers), "properties"); 41 | module.addStep("artifacts", new Download(repositories), "resolved"); 42 | }, "deps"); 43 | root.addModule("test", (module, _) -> { 44 | module.addSource("sources", Bind.asSources(), Path.of("tests")); 45 | module.addStep("classes", Javac.tool(), "sources", "../main/artifacts", "../test-deps/artifacts"); 46 | module.addStep("artifacts", Jar.tool(), "classes", "../test-deps/artifacts"); 47 | module.addStep("tests", new Tests(), "artifacts", "../main/artifacts", "../test-deps/artifacts"); 48 | }, "test-deps", "main"); 49 | 50 | root.execute(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/Repository.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.Reader; 6 | import java.net.URI; 7 | import java.net.URLEncoder; 8 | import java.nio.charset.StandardCharsets; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | import java.util.Optional; 16 | import java.util.Properties; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.ConcurrentMap; 19 | import java.util.concurrent.Executor; 20 | import java.util.function.Function; 21 | import java.util.stream.Collectors; 22 | import java.util.stream.Stream; 23 | 24 | @FunctionalInterface 25 | public interface Repository { 26 | 27 | Optional fetch(Executor executor, String coordinate) throws IOException; 28 | 29 | default Repository prepend(Repository repository) { 30 | return (executor, coordinate) -> { 31 | Optional candidate = repository.fetch(executor, coordinate); 32 | return candidate.isPresent() ? candidate : fetch(executor, coordinate); 33 | }; 34 | } 35 | 36 | default Repository cached(Path folder) { 37 | if (folder == null) { 38 | return this; 39 | } 40 | ConcurrentMap cache = new ConcurrentHashMap<>(); 41 | return (executor, coordinate) -> { 42 | Path previous = cache.get(coordinate); 43 | if (previous != null) { 44 | return Optional.of(RepositoryItem.ofFile(previous)); 45 | } 46 | RepositoryItem item = fetch(executor, coordinate).orElse(null); 47 | if (item == null) { 48 | return Optional.empty(); 49 | } else { 50 | Path file = item.getFile().orElse(null), target = folder.resolve(URLEncoder.encode( 51 | coordinate, 52 | StandardCharsets.UTF_8) + ".jar"); 53 | if (file != null) { 54 | Files.createLink(target, file); 55 | } else { 56 | try (InputStream inputStream = item.toInputStream()) { 57 | Files.copy(inputStream, target); 58 | } 59 | } 60 | Path concurrent = cache.putIfAbsent(coordinate, target); 61 | return Optional.of(RepositoryItem.ofFile(concurrent == null ? target : concurrent)); 62 | } 63 | }; 64 | } 65 | 66 | static Repository empty() { 67 | return (_, _) -> Optional.empty(); 68 | } 69 | 70 | static Repository ofUris(Map uris) { 71 | return (_, coordinate) -> { 72 | URI uri = uris.get(coordinate); 73 | if (uri == null) { 74 | return Optional.empty(); 75 | } else if (Objects.equals("file", uri.getScheme())) { 76 | return Optional.of(RepositoryItem.ofFile(Path.of(uri))); 77 | } else { 78 | return Optional.of(() -> uri.toURL().openStream()); 79 | } 80 | }; 81 | } 82 | 83 | static Repository ofFiles(Map files) { 84 | return (_, coordinate) -> { 85 | Path file = files.get(coordinate); 86 | return file == null ? Optional.empty() : Optional.of(RepositoryItem.ofFile(file)); 87 | }; 88 | } 89 | 90 | static Repository files() { 91 | return (_, coordinate) -> { 92 | Path file = Paths.get(coordinate); 93 | return Files.exists(file) ? Optional.of(RepositoryItem.ofFile(file)) : Optional.empty(); 94 | }; 95 | } 96 | 97 | static Map ofProperties(String suffix, 98 | Iterable folders, 99 | Function resolver, 100 | Path cache) throws IOException { 101 | Map> artifacts = new HashMap<>(); 102 | for (Path folder : folders) { 103 | Path file = folder.resolve(suffix); 104 | if (Files.exists(file)) { 105 | Properties properties = new SequencedProperties(); 106 | try (Reader reader = Files.newBufferedReader(file)) { 107 | properties.load(reader); 108 | } 109 | for (String coordinate : properties.stringPropertyNames()) { 110 | String location = properties.getProperty(coordinate); 111 | if (!location.isEmpty()) { 112 | int index = coordinate.indexOf('/'); 113 | artifacts.computeIfAbsent( 114 | coordinate.substring(0, index), 115 | _ -> new HashMap<>()).put(coordinate.substring(index + 1), resolver.apply(location)); 116 | } 117 | } 118 | } 119 | } 120 | return artifacts.entrySet().stream() 121 | .map(entry -> Map.entry(entry.getKey(), Repository.ofUris(entry.getValue()).cached(cache))) 122 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 123 | } 124 | 125 | static Map prepend(Map left, 126 | Map right) { 127 | return Stream.concat(left.entrySet().stream(), right.entrySet().stream()).collect(Collectors.toMap( 128 | Map.Entry::getKey, 129 | Map.Entry::getValue, 130 | Repository::prepend)); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/RepositoryItem.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.util.Optional; 8 | 9 | @FunctionalInterface 10 | public interface RepositoryItem { 11 | 12 | default Optional getFile() { 13 | return Optional.empty(); 14 | } 15 | 16 | InputStream toInputStream() throws IOException; 17 | 18 | static RepositoryItem ofFile(Path file) { 19 | return new RepositoryItem() { 20 | @Override 21 | public Optional getFile() { 22 | return Optional.of(file); 23 | } 24 | 25 | @Override 26 | public InputStream toInputStream() throws IOException { 27 | return Files.newInputStream(file); 28 | } 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/Resolver.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.IOException; 4 | import java.util.LinkedHashMap; 5 | import java.util.LinkedHashSet; 6 | import java.util.Map; 7 | import java.util.SequencedCollection; 8 | import java.util.SequencedMap; 9 | import java.util.SequencedSet; 10 | import java.util.concurrent.Executor; 11 | import java.util.function.BiFunction; 12 | import java.util.function.Function; 13 | import java.util.stream.Collectors; 14 | 15 | @FunctionalInterface 16 | public interface Resolver { 17 | 18 | SequencedMap dependencies(Executor executor, 19 | String prefix, 20 | Map repositories, 21 | SequencedSet coordinates) throws IOException; 22 | 23 | default Resolver translated(String translated, BiFunction translator) { 24 | return (executor, prefix, repositories, coordinates) -> dependencies(executor, 25 | translated, 26 | repositories, 27 | coordinates.stream() 28 | .map(coordinate -> translator.apply(prefix, coordinate)) 29 | .collect(Collectors.toCollection(LinkedHashSet::new))); 30 | } 31 | 32 | static Resolver identity() { 33 | return (_, prefix, _, coordinates) -> { 34 | SequencedMap resolved = new LinkedHashMap<>(); 35 | coordinates.forEach(coordinate -> resolved.put(prefix + "/" + coordinate, "")); 36 | return resolved; 37 | }; 38 | } 39 | 40 | static Resolver of(Function> translator) { 41 | return (_, prefix, _, coordinates) -> { 42 | SequencedMap resolved = new LinkedHashMap<>(); 43 | coordinates.stream() 44 | .flatMap(coordinate -> translator.apply(coordinate).stream()) 45 | .forEach(coordinate -> resolved.put(prefix + "/" + coordinate, "")); 46 | return resolved; 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/SequencedProperties.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.IOException; 5 | import java.io.Writer; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.Enumeration; 9 | import java.util.LinkedHashMap; 10 | import java.util.LinkedHashSet; 11 | import java.util.Map; 12 | import java.util.Properties; 13 | import java.util.SequencedMap; 14 | import java.util.SequencedSet; 15 | import java.util.Set; 16 | import java.util.function.BiConsumer; 17 | import java.util.function.BiFunction; 18 | import java.util.function.Function; 19 | 20 | public class SequencedProperties extends Properties { 21 | 22 | private final SequencedMap delegate = new LinkedHashMap<>(); 23 | 24 | @Override 25 | public void store(Writer writer, String comments) throws IOException { 26 | super.store(new CommentSuppressingWriter(writer), comments); 27 | } 28 | 29 | @Override 30 | public String getProperty(String key) { 31 | if (delegate.get(key) instanceof String value) { 32 | return value; 33 | } else if (defaults != null) { 34 | return defaults.getProperty(key); 35 | } else { 36 | return null; 37 | } 38 | } 39 | 40 | @Override 41 | public int size() { 42 | return delegate.size(); 43 | } 44 | 45 | @Override 46 | public boolean isEmpty() { 47 | return delegate.isEmpty(); 48 | } 49 | 50 | @Override 51 | public Enumeration keys() { 52 | return Collections.enumeration(delegate.keySet()); 53 | } 54 | 55 | @Override 56 | public Enumeration elements() { 57 | return Collections.enumeration(delegate.values()); 58 | } 59 | 60 | @Override 61 | public boolean contains(Object value) { 62 | return delegate.containsValue(value); 63 | } 64 | 65 | @Override 66 | public boolean containsValue(Object value) { 67 | return delegate.containsValue(value); 68 | } 69 | 70 | @Override 71 | public boolean containsKey(Object key) { 72 | return delegate.containsKey(key); 73 | } 74 | 75 | @Override 76 | public Object get(Object key) { 77 | return delegate.get(key); 78 | } 79 | 80 | @Override 81 | public Object put(Object key, Object value) { 82 | return delegate.put(key, value); 83 | } 84 | 85 | @Override 86 | public Object remove(Object key) { 87 | return delegate.remove(key); 88 | } 89 | 90 | @Override 91 | public void putAll(Map t) { 92 | delegate.putAll(t); 93 | } 94 | 95 | @Override 96 | public void clear() { 97 | delegate.clear(); 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return delegate.toString(); 103 | } 104 | 105 | @Override 106 | public Set keySet() { 107 | return delegate.keySet(); 108 | } 109 | 110 | @Override 111 | public Collection values() { 112 | return delegate.values(); 113 | } 114 | 115 | @Override 116 | public Set> entrySet() { 117 | return delegate.entrySet(); 118 | } 119 | 120 | @Override 121 | public boolean equals(Object o) { 122 | return delegate.equals(o); 123 | } 124 | 125 | @Override 126 | public int hashCode() { 127 | return delegate.hashCode(); 128 | } 129 | 130 | @Override 131 | public Object getOrDefault(Object key, Object defaultValue) { 132 | return delegate.getOrDefault(key, defaultValue); 133 | } 134 | 135 | @Override 136 | public void forEach(BiConsumer action) { 137 | delegate.forEach(action); 138 | } 139 | 140 | @Override 141 | public void replaceAll(BiFunction function) { 142 | delegate.replaceAll(function); 143 | } 144 | 145 | @Override 146 | public Object putIfAbsent(Object key, Object value) { 147 | return delegate.putIfAbsent(key, value); 148 | } 149 | 150 | @Override 151 | public boolean remove(Object key, Object value) { 152 | return delegate.remove(key, value); 153 | } 154 | 155 | @Override 156 | public boolean replace(Object key, Object oldValue, Object newValue) { 157 | return delegate.replace(key, oldValue, newValue); 158 | } 159 | 160 | @Override 161 | public Object replace(Object key, Object value) { 162 | return delegate.replace(key, value); 163 | } 164 | 165 | @Override 166 | public Object computeIfAbsent(Object key, Function mappingFunction) { 167 | return delegate.computeIfAbsent(key, mappingFunction); 168 | } 169 | 170 | @Override 171 | public Object computeIfPresent(Object key, BiFunction remappingFunction) { 172 | return delegate.computeIfPresent(key, remappingFunction); 173 | } 174 | 175 | @Override 176 | public Object compute(Object key, BiFunction remappingFunction) { 177 | return delegate.compute(key, remappingFunction); 178 | } 179 | 180 | @Override 181 | public Object merge(Object key, Object value, BiFunction remappingFunction) { 182 | return delegate.merge(key, value, remappingFunction); 183 | } 184 | 185 | @Override 186 | public Object clone() { 187 | throw new UnsupportedOperationException(); 188 | } 189 | 190 | @Override 191 | public Set stringPropertyNames() { 192 | SequencedSet keys = new LinkedHashSet<>(); 193 | delegate.forEach((key, value) -> { 194 | if (key instanceof String string && value instanceof String) { 195 | keys.add(string); 196 | } 197 | }); 198 | return keys; 199 | } 200 | 201 | private static class CommentSuppressingWriter extends BufferedWriter { 202 | 203 | private boolean skipNextNewLine; 204 | 205 | private CommentSuppressingWriter(Writer out) { 206 | super(out); 207 | } 208 | 209 | @Override 210 | public void write(String str) throws IOException { 211 | if (str.startsWith("#")) { 212 | skipNextNewLine = true; 213 | } else { 214 | super.write(str); 215 | } 216 | } 217 | 218 | @Override 219 | public void newLine() throws IOException { 220 | if (skipNextNewLine) { 221 | skipNextNewLine = false; 222 | } else { 223 | super.newLine(); 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenDependencyKey.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | public record MavenDependencyKey(String groupId, String artifactId, String type, String classifier) { 4 | } 5 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenDependencyName.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | public record MavenDependencyName(String groupId, String artifactId) { 4 | 5 | public static final MavenDependencyName EXCLUDE_ALL = new MavenDependencyName("*", "*"); 6 | } 7 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenDependencyScope.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | public enum MavenDependencyScope { 4 | 5 | COMPILE, RUNTIME, PROVIDED, TEST, SYSTEM, IMPORT; 6 | 7 | boolean reduces(MavenDependencyScope scope) { 8 | return scope != null && ordinal() > scope.ordinal(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenDependencyValue.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | 6 | public record MavenDependencyValue(String version, 7 | MavenDependencyScope scope, 8 | Path systemPath, 9 | List exclusions, 10 | Boolean optional) { 11 | } 12 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenLocalPom.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | import java.util.List; 4 | import java.util.SequencedMap; 5 | 6 | public record MavenLocalPom(String groupId, 7 | String artifactId, 8 | String version, 9 | String packaging, 10 | String sourceDirectory, 11 | List resourceDirectories, 12 | String testSourceDirectory, 13 | List testResourceDirectories, 14 | SequencedMap dependencies, 15 | SequencedMap managedDependencies) { 16 | } 17 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenPomEmitter.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | import org.w3c.dom.Document; 4 | import org.w3c.dom.Element; 5 | import org.w3c.dom.Node; 6 | 7 | import javax.xml.parsers.DocumentBuilderFactory; 8 | import javax.xml.parsers.ParserConfigurationException; 9 | import javax.xml.transform.OutputKeys; 10 | import javax.xml.transform.Transformer; 11 | import javax.xml.transform.TransformerConfigurationException; 12 | import javax.xml.transform.TransformerException; 13 | import javax.xml.transform.TransformerFactory; 14 | import javax.xml.transform.dom.DOMSource; 15 | import javax.xml.transform.stream.StreamResult; 16 | import java.io.IOException; 17 | import java.io.Writer; 18 | import java.util.Map; 19 | import java.util.Objects; 20 | import java.util.SequencedMap; 21 | 22 | public class MavenPomEmitter { 23 | 24 | private static final String NAMESPACE_4_0_0 = "http://maven.apache.org/POM/4.0.0"; 25 | 26 | private final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 27 | private final TransformerFactory transformerFactory = TransformerFactory.newInstance(); 28 | 29 | public MavenPomEmitter() { 30 | documentBuilderFactory.setNamespaceAware(true); 31 | } 32 | 33 | public IOConsumer emit(String groupId, 34 | String artifactId, 35 | String version, 36 | String packaging, 37 | SequencedMap dependencies) { 38 | Document document; 39 | try { 40 | document = documentBuilderFactory.newDocumentBuilder().newDocument(); 41 | } catch (ParserConfigurationException e) { 42 | throw new IllegalStateException(e); 43 | } 44 | Element project = (Element) document.appendChild(document.createElementNS(NAMESPACE_4_0_0, "project")); 45 | project.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance", 46 | "xsi:schemaLocation", 47 | "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"); 48 | project.appendChild(document.createElementNS(NAMESPACE_4_0_0, "modelVersion")).setTextContent("4.0.0"); 49 | project.appendChild(document.createElementNS(NAMESPACE_4_0_0, "groupId")).setTextContent(groupId); 50 | project.appendChild(document.createElementNS(NAMESPACE_4_0_0, "artifactId")).setTextContent(artifactId); 51 | project.appendChild(document.createElementNS(NAMESPACE_4_0_0, "version")).setTextContent(version); 52 | if (packaging != null) { 53 | project.appendChild(document.createElementNS(NAMESPACE_4_0_0, "packaging")).setTextContent(packaging); 54 | } 55 | if (!dependencies.isEmpty()) { 56 | Node wrapper = project.appendChild(document.createElementNS(NAMESPACE_4_0_0, "dependencies")); 57 | for (Map.Entry dependency : dependencies.entrySet()) { 58 | Node node = wrapper.appendChild(document.createElementNS(NAMESPACE_4_0_0, "dependency")); 59 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "groupId")).setTextContent(dependency.getKey().groupId()); 60 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "artifactId")).setTextContent(dependency.getKey().artifactId()); 61 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "version")).setTextContent(dependency.getValue().version()); 62 | if (!Objects.equals(dependency.getKey().type(), "jar")) { 63 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "type")).setTextContent(dependency.getKey().type()); 64 | } 65 | if (dependency.getKey().classifier() != null) { 66 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "classifier")).setTextContent(dependency.getKey().classifier()); 67 | } 68 | if (dependency.getValue().scope() != MavenDependencyScope.COMPILE) { 69 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "scope")).setTextContent(switch (dependency.getValue().scope()) { 70 | case PROVIDED -> "provided"; 71 | case RUNTIME -> "runtime"; 72 | case TEST -> "test"; 73 | case SYSTEM -> "system"; 74 | case IMPORT -> "import"; 75 | default -> throw new IllegalStateException("Unexpected scope: " + dependency.getValue().scope()); 76 | }); 77 | } 78 | if (dependency.getValue().systemPath() != null) { 79 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "systemPath")).setTextContent(dependency.getValue().systemPath().toString()); 80 | } 81 | if (dependency.getValue().optional() != null) { 82 | node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "optional")).setTextContent(dependency.getValue().optional().toString()); 83 | } 84 | if (dependency.getValue().exclusions() != null) { 85 | Node exclusions = node.appendChild(document.createElementNS(NAMESPACE_4_0_0, "exclusions")); 86 | dependency.getValue().exclusions().forEach(name -> { 87 | Node exclusion = exclusions.appendChild(document.createElementNS(NAMESPACE_4_0_0, "exclusion")); 88 | exclusion.appendChild(document.createElementNS(NAMESPACE_4_0_0, "groupId")).setTextContent(name.groupId()); 89 | exclusion.appendChild(document.createElementNS(NAMESPACE_4_0_0, "artifactId")).setTextContent(name.artifactId()); 90 | }); 91 | } 92 | } 93 | } 94 | Transformer transformer; 95 | try { 96 | transformer = transformerFactory.newTransformer(); 97 | } catch (TransformerConfigurationException e) { 98 | throw new IllegalStateException(e); 99 | } 100 | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 101 | transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); 102 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 103 | return writer -> { 104 | try { 105 | transformer.transform(new DOMSource(document), new StreamResult(writer)); 106 | } catch (TransformerException e) { 107 | throw new IOException(e); 108 | } 109 | }; 110 | } 111 | 112 | @FunctionalInterface 113 | public interface IOConsumer { 114 | 115 | void accept(Writer writer) throws IOException; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenRepository.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | import build.buildbuddy.Repository; 4 | import build.buildbuddy.RepositoryItem; 5 | 6 | import java.io.IOException; 7 | import java.util.Optional; 8 | import java.util.concurrent.Executor; 9 | 10 | @FunctionalInterface 11 | public interface MavenRepository extends Repository { 12 | 13 | @Override 14 | default Optional fetch(Executor executor, String coordinate) throws IOException { 15 | String[] elements = coordinate.split("/", 5); 16 | return switch (elements.length) { 17 | case 3 -> fetch(executor, elements[0], elements[1], elements[2], "jar", null, null); 18 | case 4 -> fetch(executor, elements[0], elements[1], elements[3], elements[2], null, null); 19 | case 5 -> fetch(executor, elements[0], elements[1], elements[4], elements[2], elements[3], null); 20 | default -> throw new IllegalArgumentException("Insufficient Maven coordinate: " + coordinate); 21 | }; 22 | } 23 | 24 | @Override 25 | default MavenRepository prepend(Repository repository) { 26 | MavenRepository mavenRepository = of(repository); 27 | return new MavenRepository() { 28 | @Override 29 | public Optional fetch(Executor executor, 30 | String groupId, 31 | String artifactId, 32 | String version, 33 | String type, 34 | String classifier, 35 | String checksum) throws IOException { 36 | Optional candidate = mavenRepository.fetch(executor, 37 | groupId, 38 | artifactId, 39 | version, 40 | type, 41 | classifier, 42 | checksum); 43 | return candidate.isPresent() 44 | ? candidate 45 | : MavenRepository.this.fetch(executor, groupId, artifactId, version, type, classifier, checksum); 46 | } 47 | 48 | @Override 49 | public Optional fetchMetadata(Executor executor, 50 | String groupId, 51 | String artifactId, 52 | String checksum) throws IOException { 53 | return MavenRepository.this.fetchMetadata(executor, groupId, artifactId, checksum); // TODO: update? 54 | } 55 | }; 56 | } 57 | 58 | Optional fetch(Executor executor, 59 | String groupId, 60 | String artifactId, 61 | String version, 62 | String type, 63 | String classifier, 64 | String checksum) throws IOException; 65 | 66 | 67 | default Optional fetchMetadata(Executor executor, 68 | String groupId, 69 | String artifactId, 70 | String checksum) throws IOException { 71 | return Optional.empty(); 72 | } 73 | static MavenRepository of(Repository repository) { 74 | return repository instanceof MavenRepository mavenRepository ? mavenRepository : (executor, 75 | groupId, 76 | artifactId, 77 | version, 78 | type, 79 | classifier, 80 | checksum) -> { 81 | if (checksum != null) { 82 | return Optional.empty(); 83 | } 84 | Optional candidate = repository.fetch(executor, groupId 85 | + "/" + artifactId 86 | + (type == null ? "/jar" : "/" + type) 87 | + "/" + version 88 | + (classifier == null ? "" : "/" + classifier)); 89 | if (type == null && candidate.isEmpty()) { 90 | candidate = repository.fetch(executor, groupId 91 | + "/" + artifactId 92 | + "/" + version 93 | + (classifier == null ? "" : "/" + classifier)); 94 | } 95 | return candidate; 96 | }; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenUriParser.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | import build.buildbuddy.SequencedProperties; 4 | 5 | import java.io.IOException; 6 | import java.io.Reader; 7 | import java.net.URI; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.Arrays; 11 | import java.util.Objects; 12 | import java.util.Properties; 13 | import java.util.function.Function; 14 | 15 | public class MavenUriParser implements Function { 16 | 17 | public static Function ofUris(MavenUriParser parser, 18 | String location, 19 | Iterable folders) throws IOException { 20 | Properties properties = new SequencedProperties(); 21 | for (Path folder : folders) { 22 | Path file = folder.resolve(location); 23 | if (Files.exists(file)) { 24 | try (Reader reader = Files.newBufferedReader(file)) { 25 | properties.load(reader); 26 | } 27 | } 28 | } 29 | return property -> { 30 | String value = properties.getProperty(property); 31 | if (value == null) { 32 | throw new IllegalArgumentException("Could not translate " + property); 33 | } 34 | return parser.apply(value); 35 | }; 36 | } 37 | 38 | @Override 39 | public String apply(String value) { 40 | URI uri = URI.create(value); 41 | String[] elements = uri.getPath().split("/"); 42 | String type = elements[elements.length - 1].substring(elements[elements.length - 1].lastIndexOf('.') + 1); 43 | String classifier = elements[elements.length - 1].substring( 44 | elements[elements.length - 3].length(), 45 | elements[elements.length - 1].length() - type.length() - elements[elements.length - 2].length() - 2); 46 | return String.join(".", Arrays.asList(elements).subList(2, elements.length - 3)) 47 | + "/" + elements[elements.length - 3] 48 | + (Objects.equals(type, "jar") && classifier.isEmpty() ? "" : "/" + type) 49 | + (classifier.isEmpty() ? "" : "/" + classifier.substring(1)) 50 | + "/" + elements[elements.length - 2]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/maven/MavenVersionNegotiator.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.maven; 2 | 3 | import java.io.IOException; 4 | import java.util.SequencedSet; 5 | import java.util.concurrent.Executor; 6 | 7 | @FunctionalInterface 8 | public interface MavenVersionNegotiator { 9 | 10 | String resolve(Executor executor, 11 | MavenRepository repository, 12 | String groupId, 13 | String artifactId, 14 | String type, 15 | String classifier, 16 | String version) throws IOException; 17 | 18 | default String resolve(Executor executor, 19 | MavenRepository repository, 20 | String groupId, 21 | String artifactId, 22 | String type, 23 | String classifier, 24 | String current, 25 | SequencedSet versions) throws IOException { 26 | return current; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/module/DownloadModuleUris.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.module; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.BufferedWriter; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.net.URI; 13 | import java.nio.charset.StandardCharsets; 14 | import java.nio.file.Files; 15 | import java.util.Iterator; 16 | import java.util.List; 17 | import java.util.SequencedMap; 18 | import java.util.concurrent.CompletableFuture; 19 | import java.util.concurrent.CompletionStage; 20 | import java.util.concurrent.Executor; 21 | 22 | public class DownloadModuleUris implements BuildStep { 23 | 24 | public static final String URIS = "uris.properties"; 25 | 26 | public static final URI DEFAULT = URI.create("https://raw.githubusercontent.com/" + 27 | "sormuras/modules/refs/heads/main/com.github.sormuras.modules/" + 28 | "com/github/sormuras/modules/modules.properties"); 29 | 30 | private final String prefix; 31 | private final List locations; 32 | 33 | public DownloadModuleUris() { 34 | prefix = "module"; 35 | locations = List.of(DEFAULT); 36 | } 37 | 38 | public DownloadModuleUris(String prefix, List locations) { 39 | this.prefix = prefix; 40 | this.locations = locations; 41 | } 42 | 43 | @Override 44 | public CompletionStage apply(Executor executor, 45 | BuildStepContext context, 46 | SequencedMap arguments) 47 | throws IOException { 48 | try (BufferedWriter writer = Files.newBufferedWriter(context.next().resolve(URIS))) { 49 | for (URI location : locations) { 50 | try (BufferedReader reader = new BufferedReader(new InputStreamReader( 51 | location.toURL().openStream(), 52 | StandardCharsets.UTF_8))) { 53 | Iterator it = reader.lines().iterator(); 54 | while (it.hasNext()) { 55 | writer.write((prefix == null ? "" : (prefix + "/")) + it.next()); 56 | writer.newLine(); 57 | } 58 | } 59 | } 60 | } 61 | return CompletableFuture.completedStage(new BuildStepResult(true)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/module/ModularJarResolver.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.module; 2 | 3 | import build.buildbuddy.Repository; 4 | import build.buildbuddy.RepositoryItem; 5 | import build.buildbuddy.Resolver; 6 | 7 | import java.io.IOException; 8 | import java.lang.module.ModuleDescriptor; 9 | import java.lang.module.ModuleFinder; 10 | import java.lang.module.ModuleReference; 11 | import java.lang.reflect.AccessFlag; 12 | import java.nio.file.Path; 13 | import java.util.ArrayDeque; 14 | import java.util.LinkedHashMap; 15 | import java.util.LinkedHashSet; 16 | import java.util.Map; 17 | import java.util.Queue; 18 | import java.util.SequencedMap; 19 | import java.util.SequencedSet; 20 | import java.util.concurrent.Executor; 21 | import java.util.zip.ZipEntry; 22 | import java.util.zip.ZipInputStream; 23 | 24 | public class ModularJarResolver implements Resolver { 25 | 26 | private final boolean resolveAutomaticModules; 27 | 28 | private final Resolver fallback; 29 | 30 | public ModularJarResolver(boolean resolveAutomaticModules) { 31 | this.resolveAutomaticModules = resolveAutomaticModules; 32 | fallback = null; 33 | } 34 | 35 | public ModularJarResolver(boolean resolveAutomaticModules, Resolver fallback) { 36 | this.resolveAutomaticModules = resolveAutomaticModules; 37 | this.fallback = fallback; 38 | } 39 | 40 | @Override 41 | public SequencedMap dependencies(Executor executor, 42 | String prefix, 43 | Map repositories, 44 | SequencedSet coordinates) throws IOException { 45 | SequencedMap dependencies = new LinkedHashMap<>(); 46 | SequencedSet unresolved = new LinkedHashSet<>(); 47 | Queue queue = new ArrayDeque<>(coordinates); 48 | while (!queue.isEmpty()) { // TODO: consider multi-release-jars better? 49 | String current = queue.remove(); 50 | RepositoryItem item = repositories.getOrDefault(prefix, Repository.empty()).fetch( 51 | executor, 52 | current).orElse(null); 53 | if (item == null) { 54 | if (fallback == null) { 55 | throw new IllegalArgumentException("No module found for " + current); 56 | } 57 | unresolved.add(current); 58 | } else { 59 | dependencies.put(prefix + "/" + current, ""); 60 | Path file = item.getFile().orElse(null); 61 | ModuleDescriptor descriptor; 62 | if (file == null) { 63 | try (ZipInputStream inputStream = new ZipInputStream(item.toInputStream())) { 64 | descriptor = toDescriptor(inputStream, current); 65 | } 66 | } else { 67 | descriptor = ModuleFinder.of(file).findAll().stream() 68 | .findFirst() 69 | .map(ModuleReference::descriptor) 70 | .orElseGet(() -> ModuleDescriptor.newAutomaticModule(current).build()); 71 | } 72 | if (descriptor.isAutomatic()) { 73 | if (resolveAutomaticModules) { 74 | continue; 75 | } 76 | throw new IllegalArgumentException("No module-info.class found for " + current); 77 | } 78 | descriptor.requires().stream() 79 | .filter(requires -> !requires.accessFlags().contains(AccessFlag.STATIC_PHASE)) 80 | .map(ModuleDescriptor.Requires::name) 81 | .filter(module -> !module.startsWith("java.") && !module.startsWith("jdk.")) 82 | .forEach(module -> { 83 | if (!unresolved.contains(module) && !dependencies.containsKey(prefix + "/" + module)) { 84 | queue.add(module); 85 | } 86 | }); 87 | } 88 | } 89 | if (!unresolved.isEmpty()) { 90 | fallback.dependencies(executor, prefix, repositories, unresolved).forEach(dependencies::putIfAbsent); 91 | } 92 | return dependencies; 93 | } 94 | 95 | private static ModuleDescriptor toDescriptor(ZipInputStream inputStream, String module) throws IOException { 96 | ZipEntry entry; 97 | while ((entry = inputStream.getNextEntry()) != null) { 98 | if (entry.getName().equals("module-info.class") 99 | || entry.getName().startsWith("META-INF/versions/") 100 | && entry.getName().endsWith("module-info.class")) { 101 | return ModuleDescriptor.read(inputStream); 102 | } 103 | } 104 | return ModuleDescriptor.newAutomaticModule(module).build(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/module/ModuleInfo.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.module; 2 | 3 | import java.util.SequencedSet; 4 | 5 | public record ModuleInfo(String coordinate, SequencedSet requires) { 6 | } 7 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/module/ModuleInfoParser.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.module; 2 | 3 | import com.sun.source.tree.CompilationUnitTree; 4 | import com.sun.source.tree.DirectiveTree; 5 | import com.sun.source.tree.ModuleTree; 6 | import com.sun.source.tree.RequiresTree; 7 | import com.sun.source.util.JavacTask; 8 | 9 | import javax.tools.JavaCompiler; 10 | import javax.tools.JavaFileObject; 11 | import javax.tools.SimpleJavaFileObject; 12 | import javax.tools.ToolProvider; 13 | import java.io.IOException; 14 | import java.io.PrintWriter; 15 | import java.io.Writer; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.LinkedHashSet; 19 | import java.util.List; 20 | import java.util.SequencedSet; 21 | 22 | import static java.util.Objects.requireNonNull; 23 | 24 | public class ModuleInfoParser { 25 | 26 | private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 27 | 28 | public ModuleInfo identify(Path moduleInfo) throws IOException { 29 | JavacTask javac = (JavacTask) compiler.getTask(new PrintWriter(Writer.nullWriter()), 30 | compiler.getStandardFileManager(null, null, null), 31 | null, 32 | null, 33 | null, 34 | List.of(new SimpleJavaFileObject(moduleInfo.toUri(), JavaFileObject.Kind.SOURCE) { 35 | @Override 36 | public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { 37 | return Files.readString(moduleInfo); 38 | } 39 | })); 40 | for (CompilationUnitTree unit : javac.parse()) { 41 | ModuleTree module = unit.getModule(); 42 | SequencedSet dependencies = new LinkedHashSet<>(); 43 | for (DirectiveTree directive : requireNonNull(module).getDirectives()) { 44 | if (directive instanceof RequiresTree requires) { 45 | String name = requires.getModuleName().toString(); 46 | if (!name.startsWith("java.") && !name.startsWith("jdk.")) { 47 | dependencies.add(name); 48 | } 49 | } 50 | } 51 | return new ModuleInfo(module.getName().toString(), dependencies); 52 | } 53 | throw new IllegalArgumentException("Expected module-info.java to contain module information"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/project/DependenciesModule.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.project; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.BuildExecutorModule; 5 | import build.buildbuddy.Repository; 6 | import build.buildbuddy.Resolver; 7 | import build.buildbuddy.step.Checksum; 8 | import build.buildbuddy.step.Download; 9 | import build.buildbuddy.step.Resolve; 10 | 11 | import java.nio.file.Path; 12 | import java.util.Map; 13 | import java.util.SequencedMap; 14 | 15 | public class DependenciesModule implements BuildExecutorModule { 16 | 17 | public static final String RESOLVED = "resolved", ARTIFACTS = "artifacts", PREPARED = "prepared"; 18 | 19 | private final Map repositories; 20 | private final Map resolvers; 21 | private final String checksum; 22 | 23 | public DependenciesModule(Map repositories, Map resolvers) { 24 | this(repositories, resolvers, null); 25 | } 26 | 27 | private DependenciesModule(Map repositories, Map resolvers, String checksum) { 28 | this.repositories = repositories; 29 | this.resolvers = resolvers; 30 | this.checksum = checksum; 31 | } 32 | 33 | public DependenciesModule computeChecksums(String algorithm) { 34 | return new DependenciesModule(repositories, resolvers, algorithm); 35 | } 36 | 37 | @Override 38 | public void accept(BuildExecutor buildExecutor, SequencedMap inherited) { 39 | if (checksum != null) { 40 | buildExecutor.addStep(PREPARED, new Resolve(repositories, resolvers), inherited.sequencedKeySet()); 41 | buildExecutor.addStep(RESOLVED, new Checksum(checksum, repositories), PREPARED); 42 | } else { 43 | buildExecutor.addStep(RESOLVED, new Resolve(repositories, resolvers), inherited.sequencedKeySet()); 44 | } 45 | buildExecutor.addStep(ARTIFACTS, new Download(repositories), RESOLVED); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/project/JavaModule.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.project; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.BuildExecutorModule; 5 | import build.buildbuddy.step.Tests; 6 | import build.buildbuddy.step.Jar; 7 | import build.buildbuddy.step.Javac; 8 | import build.buildbuddy.step.TestEngine; 9 | 10 | import java.nio.file.Path; 11 | import java.util.LinkedHashSet; 12 | import java.util.SequencedMap; 13 | import java.util.stream.Collectors; 14 | import java.util.stream.Stream; 15 | 16 | public record JavaModule(boolean process) implements BuildExecutorModule { 17 | 18 | public JavaModule() { 19 | this(false); 20 | } 21 | 22 | public static final String ARTIFACTS = "artifacts", CLASSES = "classes", TESTS = "tests"; 23 | 24 | public BuildExecutorModule testIfAvailable() { 25 | return test(null); 26 | } 27 | 28 | public BuildExecutorModule test(TestEngine engine) { 29 | return (buildExecutor, inherited) -> { 30 | TestEngine candidate = engine; 31 | if (candidate == null) { 32 | candidate = TestEngine.of(() -> inherited.values().stream().iterator()).orElse(null); 33 | } 34 | accept(buildExecutor, inherited); 35 | if (candidate != null) { 36 | buildExecutor.addStep(TESTS, new Tests(candidate), Stream.concat( 37 | Stream.of(CLASSES, ARTIFACTS), 38 | inherited.sequencedKeySet().stream()).collect(Collectors.toCollection(LinkedHashSet::new))); 39 | } 40 | }; 41 | } 42 | 43 | @Override 44 | public void accept(BuildExecutor buildExecutor, SequencedMap inherited) { 45 | buildExecutor.addStep(CLASSES, process ? Javac.process() : Javac.tool(), inherited.sequencedKeySet()); 46 | buildExecutor.addStep(ARTIFACTS, process ? Jar.process() : Jar.tool(), Stream.concat( 47 | Stream.of(CLASSES), 48 | inherited.sequencedKeySet().stream()).collect(Collectors.toCollection(LinkedHashSet::new))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/project/MultiProject.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.project; 2 | 3 | import build.buildbuddy.BuildExecutorModule; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Path; 7 | import java.util.SequencedMap; 8 | import java.util.SequencedSet; 9 | 10 | @FunctionalInterface 11 | public interface MultiProject { 12 | 13 | BuildExecutorModule module(String name, 14 | SequencedMap> dependencies, 15 | SequencedMap arguments) throws IOException; 16 | } 17 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/project/MultiProjectDependencies.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.project; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.SequencedProperties; 8 | 9 | import java.io.IOException; 10 | import java.io.Reader; 11 | import java.io.Writer; 12 | import java.nio.channels.FileChannel; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.security.MessageDigest; 16 | import java.security.NoSuchAlgorithmException; 17 | import java.util.HexFormat; 18 | import java.util.LinkedHashMap; 19 | import java.util.Map; 20 | import java.util.Properties; 21 | import java.util.SequencedMap; 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.concurrent.CompletionStage; 24 | import java.util.concurrent.Executor; 25 | import java.util.function.Predicate; 26 | 27 | public class MultiProjectDependencies implements BuildStep { 28 | 29 | private final String algorithm; 30 | private final Predicate isModule; 31 | 32 | public MultiProjectDependencies(String algorithm, Predicate isModule) { 33 | this.algorithm = algorithm; 34 | this.isModule = isModule; 35 | } 36 | 37 | @Override 38 | public CompletionStage apply(Executor executor, 39 | BuildStepContext context, 40 | SequencedMap arguments) 41 | throws IOException { 42 | SequencedMap coordinates = new LinkedHashMap<>(), dependencies = new LinkedHashMap<>(); 43 | for (Map.Entry entry : arguments.entrySet()) { 44 | if (isModule.test(entry.getKey())) { 45 | Path file = entry.getValue().folder().resolve(DEPENDENCIES); 46 | if (Files.exists(file)) { 47 | Properties properties = new SequencedProperties(); 48 | try (Reader reader = Files.newBufferedReader(file)) { 49 | properties.load(reader); 50 | } 51 | properties.stringPropertyNames().forEach(property -> { 52 | String value = properties.getProperty(property); 53 | dependencies.put(property, value); 54 | }); 55 | } 56 | } else { 57 | Path file = entry.getValue().folder().resolve(COORDINATES); 58 | if (Files.exists(file)) { 59 | Properties properties = new SequencedProperties(); 60 | try (Reader reader = Files.newBufferedReader(file)) { 61 | properties.load(reader); 62 | } 63 | for (String property : properties.stringPropertyNames()) { 64 | String value = properties.getProperty(property); 65 | if (!value.isEmpty()) { 66 | coordinates.put(property, value); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | Properties properties = new SequencedProperties(); 73 | MessageDigest digest; 74 | try { 75 | digest = MessageDigest.getInstance(algorithm); 76 | } catch (NoSuchAlgorithmException e) { 77 | throw new RuntimeException(e); 78 | } 79 | for (Map.Entry entry : dependencies.entrySet()) { 80 | String candidate = coordinates.get(entry.getKey()); 81 | String value; 82 | if (candidate != null && !candidate.isEmpty()) { 83 | try (FileChannel channel = FileChannel.open(Path.of(candidate))) { 84 | digest.update(channel.map(FileChannel.MapMode.READ_ONLY, channel.position(), channel.size())); 85 | } 86 | value = algorithm + "/" + HexFormat.of().formatHex(digest.digest()); 87 | digest.reset(); 88 | } else { 89 | value = entry.getValue(); 90 | } 91 | properties.setProperty(entry.getKey(), value); 92 | } 93 | try (Writer writer = Files.newBufferedWriter(context.next().resolve(DEPENDENCIES))) { 94 | properties.store(writer, null); 95 | } 96 | return CompletableFuture.completedStage(new BuildStepResult(true)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/project/MultiProjectModule.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.project; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.BuildExecutorModule; 5 | import build.buildbuddy.SequencedProperties; 6 | import build.buildbuddy.step.Group; 7 | 8 | import java.io.Reader; 9 | import java.net.URLEncoder; 10 | import java.nio.charset.StandardCharsets; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.Collections; 14 | import java.util.Iterator; 15 | import java.util.LinkedHashMap; 16 | import java.util.LinkedHashSet; 17 | import java.util.LinkedList; 18 | import java.util.Map; 19 | import java.util.Optional; 20 | import java.util.Properties; 21 | import java.util.Queue; 22 | import java.util.SequencedMap; 23 | import java.util.SequencedSet; 24 | import java.util.function.Function; 25 | import java.util.stream.Collectors; 26 | import java.util.stream.Stream; 27 | 28 | public class MultiProjectModule implements BuildExecutorModule { 29 | 30 | public static final String IDENTIFY = "identify", GROUP = "group", BUILD = "build", MODULE = "module"; 31 | 32 | private final BuildExecutorModule identifier; 33 | private final Function> resolver; 34 | private final Function>, MultiProject> factory; 35 | 36 | public MultiProjectModule(BuildExecutorModule identifier, 37 | Function> resolver, 38 | Function>, MultiProject> factory) { 39 | this.identifier = identifier; 40 | this.resolver = resolver; 41 | this.factory = factory; 42 | } 43 | 44 | @Override 45 | public void accept(BuildExecutor buildExecutor, SequencedMap inherited) { 46 | buildExecutor.addModule(IDENTIFY, identifier); 47 | buildExecutor.addModule(BUILD, (process, identified) -> { 48 | SequencedMap modules = new LinkedHashMap<>(); 49 | SequencedMap> identifiers = new LinkedHashMap<>(); 50 | for (String identifier : identified.sequencedKeySet()) { 51 | if (identifier.startsWith(PREVIOUS + IDENTIFY + "/")) { 52 | resolver.apply(identifier.substring(12)).ifPresent(module -> { 53 | String name = URLEncoder.encode(module, StandardCharsets.UTF_8); 54 | if (name.isEmpty()) { 55 | throw new IllegalArgumentException("Module name must not be empty"); 56 | } 57 | modules.put(identifier, name); 58 | identifiers.computeIfAbsent(name, _ -> new LinkedHashSet<>()).add(identifier); 59 | }); 60 | } 61 | } 62 | process.addStep(GROUP, 63 | new Group(identifier -> Optional.of(modules.get(identifier))), 64 | modules.sequencedKeySet()); 65 | process.addModule(MODULE, (build, paths) -> { 66 | SequencedMap> projects = new LinkedHashMap<>(); 67 | Path groups = paths.get(PREVIOUS + GROUP).resolve(Group.GROUPS); 68 | for (Map.Entry> entry : identifiers.entrySet()) { 69 | Properties properties = new SequencedProperties(); 70 | try (Reader reader = Files.newBufferedReader(groups.resolve(entry.getKey() + ".properties"))) { 71 | properties.load(reader); 72 | } 73 | projects.put(entry.getKey(), new LinkedHashSet<>(properties.stringPropertyNames())); 74 | } 75 | MultiProject project = factory.apply(projects); 76 | SequencedMap> pending = new LinkedHashMap<>(projects); 77 | while (!pending.isEmpty()) { 78 | Iterator>> it = pending.entrySet().iterator(); 79 | while (it.hasNext()) { 80 | Map.Entry> entry = it.next(); 81 | if (Collections.disjoint(entry.getValue(), pending.keySet())) { 82 | SequencedMap arguments = new LinkedHashMap<>(); 83 | identifiers.get(entry.getKey()).forEach(identifier -> arguments.put( 84 | PREVIOUS + identifier, 85 | paths.get(PREVIOUS + identifier))); 86 | SequencedMap> dependencies = new LinkedHashMap<>(); 87 | Queue queue = new LinkedList<>(entry.getValue()); 88 | while (!queue.isEmpty()) { 89 | String current = queue.remove(); 90 | if (!dependencies.containsKey(current)) { 91 | SequencedSet values = projects.get(current); 92 | dependencies.put(current, values); 93 | queue.addAll(values); 94 | } 95 | } 96 | build.addModule(entry.getKey(), project.module(entry.getKey(), 97 | dependencies, 98 | arguments), Stream.of( 99 | arguments.sequencedKeySet().stream(), 100 | dependencies.sequencedKeySet().stream(), 101 | inherited.sequencedKeySet().stream() 102 | .map(identifier -> PREVIOUS.repeat(2) + identifier)) 103 | .flatMap(Function.identity()) 104 | .collect(Collectors.toCollection(LinkedHashSet::new))); 105 | it.remove(); 106 | } 107 | } 108 | } 109 | }, Stream.concat( 110 | Stream.of(GROUP), 111 | identified.sequencedKeySet().stream()).collect(Collectors.toCollection(LinkedHashSet::new))); 112 | }, Stream.concat( 113 | Stream.of(IDENTIFY), 114 | inherited.sequencedKeySet().stream()).collect(Collectors.toCollection(LinkedHashSet::new))); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Assign.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.SequencedProperties; 8 | 9 | import java.io.IOException; 10 | import java.io.Reader; 11 | import java.io.Writer; 12 | import java.nio.file.DirectoryStream; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.util.LinkedHashSet; 16 | import java.util.Map; 17 | import java.util.Properties; 18 | import java.util.SequencedMap; 19 | import java.util.SequencedSet; 20 | import java.util.Set; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.CompletionStage; 23 | import java.util.concurrent.Executor; 24 | import java.util.function.BiFunction; 25 | import java.util.function.Function; 26 | import java.util.stream.Collectors; 27 | 28 | public class Assign implements BuildStep { 29 | 30 | private final BiFunction, SequencedSet, Map> assigner; 31 | 32 | public Assign() { 33 | assigner = (coordinates, files) -> { 34 | if (files.size() != 1) { 35 | throw new IllegalArgumentException("Expected exactly one artifact: " + files); 36 | } 37 | return coordinates.stream().collect(Collectors.toMap(Function.identity(), _ -> files.getFirst())); 38 | }; 39 | } 40 | 41 | public Assign(BiFunction, SequencedSet, Map> assigner) { 42 | this.assigner = assigner; 43 | } 44 | 45 | @Override 46 | public CompletionStage apply(Executor executor, 47 | BuildStepContext context, 48 | SequencedMap arguments) 49 | throws IOException { 50 | // TODO: improve incremental resolve 51 | Properties assignments = new SequencedProperties(); 52 | SequencedSet files = new LinkedHashSet<>(); 53 | for (BuildStepArgument argument : arguments.values()) { 54 | Path artifacts = argument.folder().resolve(ARTIFACTS); 55 | if (Files.exists(artifacts)) { 56 | try (DirectoryStream stream = Files.newDirectoryStream(artifacts)) { 57 | for (Path artifact : stream) { 58 | files.add(artifact); 59 | } 60 | } 61 | } 62 | Path coordinates = argument.folder().resolve(COORDINATES); 63 | if (Files.exists(coordinates)) { 64 | Properties properties = new SequencedProperties(); 65 | try (Reader reader = Files.newBufferedReader(coordinates)) { 66 | properties.load(reader); 67 | } 68 | properties.stringPropertyNames().forEach(name -> assignments.put(name, properties.getProperty(name))); 69 | } 70 | } 71 | assigner.apply(assignments.stringPropertyNames().stream() 72 | .filter(assignment -> assignments.getProperty(assignment).isEmpty()) 73 | .collect(Collectors.toCollection(LinkedHashSet::new)), files).forEach((coordinate, path) -> { 74 | if (!files.contains(path)) { 75 | throw new IllegalArgumentException("Unknown path " + path); 76 | } 77 | assignments.setProperty(coordinate, path.toString()); 78 | }); 79 | try (Writer writer = Files.newBufferedWriter(context.next().resolve(COORDINATES))) { 80 | assignments.store(writer, null); 81 | } 82 | return CompletableFuture.completedStage(new BuildStepResult(true)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Bind.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.FileVisitResult; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.SimpleFileVisitor; 13 | import java.nio.file.attribute.BasicFileAttributes; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | import java.util.SequencedMap; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.CompletionStage; 19 | import java.util.concurrent.Executor; 20 | 21 | public class Bind implements BuildStep { 22 | 23 | private final Map paths; 24 | 25 | public Bind(Map paths) { 26 | this.paths = paths; 27 | } 28 | 29 | public static Bind asSources() { 30 | return new Bind(Map.of(Path.of("."), Path.of(SOURCES))); 31 | } 32 | 33 | public static Bind asResources() { 34 | return new Bind(Map.of(Path.of("."), Path.of(RESOURCES))); 35 | } 36 | 37 | public static Bind asCoordinates(String name) { 38 | return new Bind(Map.of(Path.of(name == null ? COORDINATES : name), Path.of(COORDINATES))); 39 | } 40 | 41 | public static Bind asDependencies(String name) { 42 | return new Bind(Map.of(Path.of(name), Path.of(DEPENDENCIES))); 43 | } 44 | 45 | @Override 46 | public boolean shouldRun(SequencedMap arguments) { 47 | return arguments.values().stream().anyMatch(argument -> argument.hasChanged(paths.keySet())); 48 | } 49 | 50 | @Override 51 | public CompletionStage apply(Executor executor, 52 | BuildStepContext context, 53 | SequencedMap arguments) 54 | throws IOException { 55 | for (BuildStepArgument argument : arguments.values()) { 56 | for (Map.Entry entry : paths.entrySet()) { 57 | Path source = argument.folder().resolve(entry.getKey()); 58 | if (Files.exists(source)) { 59 | Path target = context.next().resolve(entry.getValue()); 60 | if (!Objects.equals(target.getParent(), context.next())) { 61 | Files.createDirectories(target.getParent()); 62 | } 63 | Files.walkFileTree(source, new SimpleFileVisitor<>() { 64 | @Override 65 | public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 66 | Files.createDirectories(target.resolve(source.relativize(dir))); 67 | return FileVisitResult.CONTINUE; 68 | } 69 | 70 | @Override 71 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 72 | Files.createLink(target.resolve(source.relativize(file)), file); 73 | return FileVisitResult.CONTINUE; 74 | } 75 | }); 76 | } 77 | } 78 | } 79 | return CompletableFuture.completedStage(new BuildStepResult(true)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Checksum.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | import build.buildbuddy.Repository; 6 | import build.buildbuddy.RepositoryItem; 7 | import build.buildbuddy.SequencedProperties; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.nio.channels.FileChannel; 12 | import java.nio.file.Path; 13 | import java.security.MessageDigest; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.util.HexFormat; 16 | import java.util.Map; 17 | import java.util.Properties; 18 | import java.util.SequencedMap; 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.CompletionStage; 21 | import java.util.concurrent.Executor; 22 | 23 | public class Checksum implements DependencyTransformingBuildStep { 24 | 25 | private final String algorithm; 26 | private final Map repositories; 27 | 28 | public Checksum(String algorithm, Map repositories) { 29 | this.algorithm = algorithm; 30 | this.repositories = repositories; 31 | } 32 | 33 | @Override 34 | public CompletionStage transform(Executor executor, 35 | BuildStepContext context, 36 | SequencedMap arguments, 37 | SequencedMap> groups) 38 | throws IOException { 39 | Properties properties = new SequencedProperties(); 40 | for (Map.Entry> group : groups.entrySet()) { 41 | Repository repository = repositories.getOrDefault(group.getKey(), Repository.empty()); 42 | for (Map.Entry entry : group.getValue().entrySet()) { 43 | String dependency = group.getKey() + "/" + entry.getKey(); 44 | if (!entry.getValue().isEmpty()) { 45 | properties.setProperty(dependency, entry.getValue()); 46 | } else { 47 | MessageDigest digest; 48 | try { 49 | digest = MessageDigest.getInstance(algorithm); 50 | } catch (NoSuchAlgorithmException e) { 51 | throw new RuntimeException(e); 52 | } 53 | RepositoryItem item = repository.fetch(executor, entry.getKey()).orElseThrow( 54 | () -> new IllegalStateException("Cannot resolve " + dependency)); 55 | Path file = item.getFile().orElse(null); 56 | if (file == null) { 57 | try (InputStream inputStream = item.toInputStream()) { 58 | byte[] buffer = new byte[1024 * 8]; 59 | int length; 60 | while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) { 61 | digest.update(buffer, 0, length); 62 | } 63 | } 64 | } else { 65 | try (FileChannel channel = FileChannel.open(file)) { 66 | digest.update(channel.map( 67 | FileChannel.MapMode.READ_ONLY, channel.position(), channel.size())); 68 | } 69 | } 70 | properties.setProperty(dependency, algorithm + "/" + HexFormat.of().formatHex(digest.digest())); 71 | } 72 | } 73 | } 74 | return CompletableFuture.completedStage(properties); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/DependencyTransformingBuildStep.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.SequencedProperties; 8 | 9 | import java.io.IOException; 10 | import java.io.Reader; 11 | import java.io.Writer; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.util.LinkedHashMap; 15 | import java.util.Properties; 16 | import java.util.SequencedMap; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.CompletionStage; 19 | import java.util.concurrent.Executor; 20 | 21 | @FunctionalInterface 22 | public interface DependencyTransformingBuildStep extends BuildStep { 23 | 24 | @Override 25 | default CompletionStage apply(Executor executor, 26 | BuildStepContext context, 27 | SequencedMap arguments) 28 | throws IOException { 29 | // TODO: improve incremental resolve 30 | SequencedMap> groups = new LinkedHashMap<>(); 31 | for (BuildStepArgument argument : arguments.values()) { 32 | Path dependencies = argument.folder().resolve(DEPENDENCIES); 33 | if (!Files.exists(dependencies)) { 34 | continue; 35 | } 36 | Properties properties = new SequencedProperties(); 37 | try (Reader reader = Files.newBufferedReader(dependencies)) { 38 | properties.load(reader); 39 | } 40 | for (String property : properties.stringPropertyNames()) { 41 | int index = property.indexOf('/'); 42 | groups.computeIfAbsent(property.substring(0, index), _ -> new LinkedHashMap<>()).merge( 43 | property.substring(index + 1), 44 | properties.getProperty(property), 45 | (left, right) -> left.isEmpty() ? right : left); 46 | } 47 | } 48 | return transform(executor, context, arguments, groups).thenComposeAsync(properties -> { 49 | CompletableFuture result = new CompletableFuture<>(); 50 | try (Writer writer = Files.newBufferedWriter(context.next().resolve(DEPENDENCIES))) { 51 | properties.store(writer, null); 52 | result.complete(new BuildStepResult(true)); 53 | } catch (Throwable t) { 54 | result.completeExceptionally(t); 55 | } 56 | return result; 57 | }, executor); 58 | } 59 | 60 | CompletionStage transform(Executor executor, 61 | BuildStepContext context, 62 | SequencedMap arguments, 63 | SequencedMap> groups) throws IOException; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Download.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | import build.buildbuddy.Repository; 6 | import build.buildbuddy.RepositoryItem; 7 | import build.buildbuddy.SequencedProperties; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.nio.channels.FileChannel; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.security.DigestInputStream; 15 | import java.security.MessageDigest; 16 | import java.security.NoSuchAlgorithmException; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.HexFormat; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.Properties; 23 | import java.util.SequencedMap; 24 | import java.util.concurrent.CompletableFuture; 25 | import java.util.concurrent.CompletionStage; 26 | import java.util.concurrent.Executor; 27 | 28 | public class Download implements DependencyTransformingBuildStep { 29 | 30 | private final Map repositories; 31 | 32 | public Download(Map repositories) { 33 | this.repositories = repositories; 34 | } 35 | 36 | @Override 37 | public CompletionStage transform(Executor executor, 38 | BuildStepContext context, 39 | SequencedMap arguments, 40 | SequencedMap> groups) 41 | throws IOException { 42 | List> futures = new ArrayList<>(); 43 | Properties properties = new SequencedProperties(); 44 | Path libs = Files.createDirectory(context.next().resolve(ARTIFACTS)); 45 | for (Map.Entry> group : groups.entrySet()) { 46 | Repository repository = repositories.getOrDefault(group.getKey(), Repository.empty()); 47 | for (Map.Entry entry : group.getValue().entrySet()) { 48 | String dependency = group.getKey() + "/" + entry.getKey(), name = dependency.replace('/', '-') + ".jar"; 49 | Path previous = context.previous() == null ? null : context.previous().resolve(ARTIFACTS + name); 50 | if (entry.getValue().isEmpty()) { 51 | if (previous != null && Files.exists(previous)) { 52 | Files.createLink(libs.resolve(name), previous); 53 | } else { 54 | CompletableFuture future = new CompletableFuture<>(); 55 | executor.execute(() -> { 56 | try { 57 | RepositoryItem source = repository.fetch(executor, entry.getKey()).orElseThrow( 58 | () -> new IllegalStateException("Unresolved: " + dependency)); 59 | Path file = source.getFile().orElse(null); 60 | if (file == null) { 61 | try (InputStream inputStream = source.toInputStream()) { 62 | Files.copy(inputStream, libs.resolve(name)); 63 | } 64 | } else { 65 | Files.createLink(context.next().resolve(ARTIFACTS + name), file); 66 | } 67 | future.complete(null); 68 | } catch (Throwable t) { 69 | future.completeExceptionally(new RuntimeException( 70 | "Failed to fetch " + dependency, 71 | t)); 72 | } 73 | }); 74 | futures.add(future); 75 | } 76 | } else { 77 | int algorithm = entry.getValue().indexOf('/'); 78 | MessageDigest digest; 79 | try { 80 | digest = MessageDigest.getInstance(entry.getValue().substring(0, algorithm)); 81 | } catch (NoSuchAlgorithmException e) { 82 | throw new IllegalStateException(e); 83 | } 84 | String checksum = entry.getValue().substring(algorithm + 1); 85 | if (previous != null && Files.exists(previous)) { 86 | if (validateFile(digest, previous, checksum)) { 87 | Files.createLink(libs.resolve(name), previous); 88 | continue; 89 | } else { 90 | digest.reset(); 91 | } 92 | } 93 | CompletableFuture future = new CompletableFuture<>(); 94 | executor.execute(() -> { 95 | try { 96 | RepositoryItem source = repository.fetch(executor, entry.getKey()).orElseThrow( 97 | () -> new IllegalStateException("Unresolved: " + dependency)); 98 | Path file = source.getFile().orElse(null); 99 | if (file == null) { 100 | try (DigestInputStream inputStream = new DigestInputStream( 101 | source.toInputStream(), 102 | digest)) { 103 | Files.copy(inputStream, libs.resolve(name)); 104 | if (!Arrays.equals( 105 | inputStream.getMessageDigest().digest(), 106 | HexFormat.of().parseHex(checksum))) { 107 | throw new IllegalStateException("Mismatched digest for " + dependency); 108 | } 109 | } 110 | } else { 111 | if (validateFile(digest, file, checksum)) { 112 | Files.createLink(context.next().resolve(ARTIFACTS + name), file); 113 | } else { 114 | throw new IllegalStateException("Mismatched digest for " + dependency); 115 | } 116 | } 117 | future.complete(null); 118 | } catch (Throwable t) { 119 | future.completeExceptionally(new RuntimeException( 120 | "Failed to fetch " + dependency, 121 | t)); 122 | } 123 | }); 124 | futures.add(future); 125 | } 126 | } 127 | } 128 | return CompletableFuture 129 | .allOf(futures.toArray(CompletableFuture[]::new)) 130 | .thenApply(_ -> properties); 131 | } 132 | 133 | private static boolean validateFile(MessageDigest digest, Path file, String expected) throws IOException { 134 | try (FileChannel channel = FileChannel.open(file)) { 135 | digest.update(channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size())); 136 | } 137 | return Arrays.equals(digest.digest(), HexFormat.of().parseHex(expected)); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Group.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.SequencedProperties; 8 | 9 | import java.io.IOException; 10 | import java.io.Reader; 11 | import java.io.Writer; 12 | import java.net.URLEncoder; 13 | import java.nio.charset.StandardCharsets; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.util.HashMap; 17 | import java.util.LinkedHashMap; 18 | import java.util.LinkedHashSet; 19 | import java.util.Map; 20 | import java.util.Optional; 21 | import java.util.Properties; 22 | import java.util.SequencedMap; 23 | import java.util.Set; 24 | import java.util.concurrent.CompletableFuture; 25 | import java.util.concurrent.CompletionStage; 26 | import java.util.concurrent.Executor; 27 | import java.util.function.Function; 28 | 29 | public class Group implements BuildStep { 30 | 31 | public static final String GROUPS = "groups/"; 32 | 33 | private final Function> identification; 34 | 35 | public Group(Function> identification) { 36 | this.identification = identification; 37 | } 38 | 39 | @Override 40 | public CompletionStage apply(Executor executor, 41 | BuildStepContext context, 42 | SequencedMap arguments) 43 | throws IOException { 44 | // TODO: improve incremental resolve 45 | Map> from = new HashMap<>(), to = new LinkedHashMap<>(); 46 | for (Map.Entry entry : arguments.entrySet()) { 47 | String name = identification.apply(entry.getKey()).orElse(null); 48 | if (name == null) { 49 | continue; 50 | } 51 | toProperties(entry.getValue().folder().resolve(COORDINATES)).forEach(dependency -> from.computeIfAbsent( 52 | dependency, 53 | _ -> new LinkedHashSet<>()).add(name)); 54 | to.computeIfAbsent(name, _ -> new LinkedHashSet<>()).addAll(toProperties(entry.getValue() 55 | .folder() 56 | .resolve(DEPENDENCIES))); 57 | } 58 | Path folder = Files.createDirectory(context.next().resolve(GROUPS)); 59 | for (Map.Entry> entry : to.entrySet()) { 60 | Properties properties = new SequencedProperties(); 61 | entry.getValue().stream() 62 | .flatMap(dependency -> from.getOrDefault(dependency, Set.of()).stream()) 63 | .distinct() 64 | .forEach(name -> properties.setProperty(name, "")); 65 | try (Writer writer = Files.newBufferedWriter(folder.resolve(URLEncoder.encode( 66 | entry.getKey(), 67 | StandardCharsets.UTF_8) + ".properties"))) { 68 | properties.store(writer, null); 69 | } 70 | } 71 | return CompletableFuture.completedStage(new BuildStepResult(true)); 72 | } 73 | 74 | private static Set toProperties(Path file) throws IOException { 75 | if (Files.exists(file)) { 76 | Properties properties = new SequencedProperties(); 77 | try (Reader reader = Files.newBufferedReader(file)) { 78 | properties.load(reader); 79 | } 80 | return properties.stringPropertyNames(); 81 | } else { 82 | return Set.of(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Jar.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.SequencedMap; 12 | import java.util.concurrent.CompletableFuture; 13 | import java.util.concurrent.CompletionStage; 14 | import java.util.concurrent.Executor; 15 | import java.util.function.Function; 16 | 17 | public class Jar extends ProcessBuildStep { 18 | 19 | public Jar(Function, ? extends ProcessHandler> factory) { 20 | super(factory); 21 | } 22 | 23 | public static Jar tool() { 24 | return new Jar(ProcessHandler.OfTool.of("jar")); 25 | } 26 | 27 | public static Jar process() { 28 | return new Jar(ProcessHandler.OfProcess.ofJavaHome("bin/jar")); 29 | } 30 | 31 | @Override 32 | public CompletionStage> process(Executor executor, 33 | BuildStepContext context, 34 | SequencedMap arguments) 35 | throws IOException { 36 | List commands = new ArrayList<>(List.of("cf", Files 37 | .createDirectory(context.next().resolve(ARTIFACTS)) 38 | .resolve("classes.jar") 39 | .toString())); 40 | for (BuildStepArgument argument : arguments.values()) { 41 | for (String name : List.of(Javac.CLASSES, Bind.RESOURCES)) { 42 | Path folder = argument.folder().resolve(name); 43 | if (Files.exists(folder)) { 44 | commands.add("-C"); 45 | commands.add(folder.toString()); 46 | commands.add("."); 47 | } 48 | } 49 | } 50 | return CompletableFuture.completedStage(commands); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Java.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.FileVisitResult; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.SimpleFileVisitor; 12 | import java.nio.file.attribute.BasicFileAttributes; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.SequencedMap; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.CompletionStage; 18 | import java.util.concurrent.Executor; 19 | import java.util.function.Function; 20 | import java.util.jar.JarFile; 21 | import java.util.stream.Stream; 22 | import java.util.zip.ZipFile; 23 | 24 | public abstract class Java extends ProcessBuildStep { 25 | 26 | protected boolean modular = true, jarsOnly = false; 27 | 28 | protected Java() { 29 | super(ProcessHandler.OfProcess.ofJavaHome("bin/java")); 30 | } 31 | 32 | protected Java(Function, ? extends ProcessHandler> factory) { 33 | super(factory); 34 | } 35 | 36 | public static Java of(String... commands) { 37 | return of(List.of(commands)); 38 | } 39 | 40 | public static Java of(List commands) { 41 | return new Java() { 42 | @Override 43 | protected CompletionStage> commands(Executor executor, 44 | BuildStepContext context, 45 | SequencedMap arguments) { 46 | return CompletableFuture.completedStage(commands); 47 | } 48 | }; 49 | } 50 | 51 | public static Java of(Function, ProcessHandler.OfProcess> factory, String... commands) { 52 | return of(factory, List.of(commands)); 53 | } 54 | 55 | public static Java of(Function, ProcessHandler.OfProcess> factory, List commands) { 56 | return new Java(factory) { 57 | @Override 58 | protected CompletionStage> commands(Executor executor, 59 | BuildStepContext context, 60 | SequencedMap arguments) { 61 | return CompletableFuture.completedStage(commands); 62 | } 63 | }; 64 | } 65 | 66 | public Java modular(boolean modular) { 67 | this.modular = modular; 68 | return this; 69 | } 70 | 71 | public Java jarsOnly(boolean jarsOnly) { 72 | this.jarsOnly = jarsOnly; 73 | return this; 74 | } 75 | 76 | protected abstract CompletionStage> commands(Executor executor, 77 | BuildStepContext context, 78 | SequencedMap arguments) throws IOException; 79 | 80 | @Override 81 | public CompletionStage> process(Executor executor, 82 | BuildStepContext context, 83 | SequencedMap arguments) 84 | throws IOException { 85 | List classPath = new ArrayList<>(), modulePath = new ArrayList<>(); 86 | for (BuildStepArgument argument : arguments.values()) { 87 | if (!jarsOnly) { 88 | for (String folder : List.of(Javac.CLASSES, Bind.RESOURCES)) { 89 | Path candidate = argument.folder().resolve(folder); 90 | if (Files.isDirectory(candidate)) { 91 | if (modular && Files.exists(candidate.resolve("module-info.class")) ) { // TODO: multi-release? 92 | modulePath.add(candidate.toString()); // TODO: does manifest apply without jar file? 93 | } else { 94 | classPath.add(candidate.toString()); 95 | } 96 | } 97 | } 98 | } 99 | Path candidate = argument.folder().resolve(ARTIFACTS); 100 | if (Files.exists(candidate)) { 101 | Files.walkFileTree(candidate, new SimpleFileVisitor<>() { 102 | @Override 103 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 104 | if (modular) { 105 | try (JarFile jar = new JarFile(file.toFile(), 106 | true, 107 | ZipFile.OPEN_READ, 108 | Runtime.version())) { // TODO: multi-release? 109 | if (jar.getEntry("module-info.class") != null 110 | || jar.getManifest() != null 111 | && jar.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null) { 112 | modulePath.add(file.toString()); 113 | return FileVisitResult.CONTINUE; 114 | } 115 | } catch (IllegalArgumentException _) { 116 | } 117 | } 118 | classPath.add(file.toString()); 119 | return FileVisitResult.CONTINUE; 120 | } 121 | }); 122 | } 123 | } 124 | List prefixes = new ArrayList<>(); 125 | if (!classPath.isEmpty()) { 126 | prefixes.add("-classpath"); 127 | prefixes.add(String.join(File.pathSeparator, classPath)); 128 | } 129 | if (!modulePath.isEmpty()) { 130 | prefixes.add("--module-path"); 131 | prefixes.add(String.join(File.pathSeparator, modulePath)); 132 | } 133 | return commands(executor, context, arguments).thenApplyAsync(commands -> Stream.concat( 134 | prefixes.stream(), 135 | commands.stream()).toList(), executor); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Javac.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.FileVisitResult; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.SimpleFileVisitor; 12 | import java.nio.file.attribute.BasicFileAttributes; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.SequencedMap; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.CompletionStage; 18 | import java.util.concurrent.Executor; 19 | import java.util.function.Function; 20 | 21 | public class Javac extends ProcessBuildStep { 22 | 23 | protected Javac(Function, ? extends ProcessHandler> factory) { 24 | super(factory); 25 | } 26 | 27 | public static Javac tool() { 28 | return new Javac(ProcessHandler.OfTool.of("javac")); 29 | } 30 | 31 | public static Javac process() { 32 | return new Javac(ProcessHandler.OfProcess.ofJavaHome("bin/javac")); 33 | } 34 | 35 | @Override 36 | public CompletionStage> process(Executor executor, 37 | BuildStepContext context, 38 | SequencedMap arguments) 39 | throws IOException { 40 | Path target = Files.createDirectory(context.next().resolve(CLASSES)); 41 | List files = new ArrayList<>(), path = new ArrayList<>(), commands = new ArrayList<>(List.of( 42 | "--release", Integer.toString(Runtime.version().version().getFirst()), 43 | "-d", target.toString())); 44 | for (BuildStepArgument argument : arguments.values()) { 45 | Path sources = argument.folder().resolve(Bind.SOURCES), 46 | classes = argument.folder().resolve(CLASSES), 47 | artifacts = argument.folder().resolve(ARTIFACTS); 48 | if (Files.exists(classes)) { 49 | path.add(classes.toString()); 50 | } 51 | if (Files.exists(artifacts)) { 52 | Files.walkFileTree(artifacts, new SimpleFileVisitor<>() { 53 | @Override 54 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 55 | path.add(file.toString()); 56 | return FileVisitResult.CONTINUE; 57 | } 58 | }); 59 | } 60 | if (Files.exists(sources)) { 61 | Files.walkFileTree(sources, new SimpleFileVisitor<>() { 62 | @Override 63 | public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 64 | Files.createDirectories(target.resolve(sources.relativize(dir))); 65 | return FileVisitResult.CONTINUE; 66 | } 67 | 68 | @Override 69 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 70 | String name = file.toString(); 71 | if (name.endsWith(".java")) { 72 | files.add(name); 73 | } else { 74 | Files.createLink(target.resolve(sources.relativize(file)), file); 75 | } 76 | return FileVisitResult.CONTINUE; 77 | } 78 | }); 79 | } 80 | } 81 | if (!path.isEmpty()) { 82 | commands.add(files.stream().anyMatch(file -> file.endsWith("/module-info.java")) 83 | ? "--module-path" 84 | : "--class-path"); // TODO: improve 85 | commands.add(String.join(File.pathSeparator, path)); // TODO: escape path 86 | } 87 | commands.addAll(files); 88 | return CompletableFuture.completedStage(commands); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/ProcessBuildStep.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.util.List; 12 | import java.util.SequencedMap; 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.concurrent.CompletionStage; 15 | import java.util.concurrent.Executor; 16 | import java.util.function.Function; 17 | 18 | public abstract class ProcessBuildStep implements BuildStep { 19 | 20 | static { 21 | if (System.getProperty("java.home") == null) { 22 | String home = System.getenv("JAVA_HOME"); 23 | if (home == null) { 24 | throw new IllegalStateException("Neither java.home or JAVA_HOME available"); 25 | } 26 | System.setProperty("java.home", home); 27 | } 28 | } 29 | 30 | private final Function, ? extends ProcessHandler> factory; 31 | 32 | protected ProcessBuildStep(Function, ? extends ProcessHandler> factory) { 33 | this.factory = factory; 34 | } 35 | 36 | protected abstract CompletionStage> process(Executor executor, 37 | BuildStepContext context, 38 | SequencedMap arguments) 39 | throws IOException; 40 | 41 | public boolean acceptableExitCode(int code, 42 | Executor executor, 43 | BuildStepContext context, 44 | SequencedMap arguments) throws IOException { 45 | return code == 0; 46 | } 47 | 48 | @Override 49 | public CompletionStage apply(Executor executor, 50 | BuildStepContext context, 51 | SequencedMap arguments) 52 | throws IOException { 53 | return process(executor, context, arguments).thenComposeAsync(arguemnts -> { 54 | CompletableFuture future = new CompletableFuture<>(); 55 | try { 56 | Path output = context.supplement().resolve("output"), error = context.supplement().resolve("error"); 57 | ProcessHandler handler = factory.apply(arguemnts); 58 | executor.execute(() -> { 59 | try { 60 | int exitCode = handler.execute(output, error); 61 | if (acceptableExitCode(exitCode, executor, context, arguments)) { 62 | future.complete(new BuildStepResult(true)); 63 | } else { 64 | String outputString = Files.exists(output) ? Files.readString(output) : ""; 65 | String errorString = Files.exists(error) ? Files.readString(error) : ""; 66 | throw new IllegalStateException("Unexpected exit code: " + exitCode + "\n" 67 | + "To reproduce, execute:\n " + String.join(" ", handler.commands()) 68 | + (outputString.isBlank() ? "" : ("\n\nOutput:\n" + outputString)) 69 | + (errorString.isBlank() ? "" : ("\n\nError:\n" + errorString))); 70 | } 71 | } catch (Throwable t) { 72 | future.completeExceptionally(t); 73 | } 74 | }); 75 | return future; 76 | } catch (Throwable t) { 77 | future.completeExceptionally(t); 78 | } 79 | return future; 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/ProcessHandler.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.PrintWriter; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.List; 9 | import java.util.function.Function; 10 | import java.util.spi.ToolProvider; 11 | import java.util.stream.Stream; 12 | 13 | public sealed interface ProcessHandler permits ProcessHandler.OfTool, ProcessHandler.OfProcess { 14 | 15 | List commands(); 16 | 17 | int execute(Path output, Path error) throws IOException; 18 | 19 | final class OfTool implements ProcessHandler { 20 | 21 | private final ToolProvider toolProvider; 22 | 23 | private final List commands; 24 | 25 | private OfTool(ToolProvider toolProvider, List commands) { 26 | this.toolProvider = toolProvider; 27 | this.commands = commands; 28 | } 29 | 30 | public static Function, ProcessHandler> of(ToolProvider toolProvider) { 31 | return arguments -> new OfTool(toolProvider, arguments); 32 | } 33 | 34 | public static Function, ProcessHandler> of(String name) { 35 | return of(ToolProvider.findFirst(name).orElseThrow(() -> new IllegalArgumentException("No tool: " + name))); 36 | } 37 | 38 | @Override 39 | public List commands() { 40 | return Stream.concat(Stream.of(toolProvider.name()), commands.stream()).toList(); 41 | } 42 | 43 | @Override 44 | public int execute(Path output, Path error) throws IOException { 45 | try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(output)); 46 | PrintWriter err = new PrintWriter(Files.newBufferedWriter(error))) { 47 | return toolProvider.run(out, err, commands.toArray(String[]::new)); 48 | } 49 | } 50 | } 51 | 52 | final class OfProcess implements ProcessHandler { 53 | 54 | private static final boolean WINDOWS = System.getProperty("os.name", "").toLowerCase().contains("win"); 55 | 56 | private final List commands; 57 | 58 | private OfProcess(List commands) { 59 | this.commands = commands; 60 | } 61 | 62 | static Function, OfProcess> ofJavaHome(String command) { 63 | String home = System.getProperty("java.home"); 64 | if (home == null) { 65 | home = System.getenv("JAVA_HOME"); 66 | } 67 | if (home == null) { 68 | throw new IllegalStateException("Neither JAVA_HOME environment or java.home property set"); 69 | } else { 70 | File program = new File(home, command); 71 | if (program.isFile()) { 72 | return of(List.of(program.getPath() + (WINDOWS ? ".exe" : ""))); 73 | } else { 74 | throw new IllegalStateException("Could not find command " + command + " in " + home); 75 | } 76 | } 77 | } 78 | 79 | public static Function, OfProcess> of(List program) { 80 | return arguments -> new OfProcess(Stream.concat(program.stream(), arguments.stream()).toList()); 81 | } 82 | 83 | @Override 84 | public List commands() { 85 | return commands; 86 | } 87 | 88 | @Override 89 | public int execute(Path output, Path error) throws IOException { 90 | Process process = new ProcessBuilder(commands) 91 | .redirectOutput(output.toFile()) 92 | .redirectError(error.toFile()) 93 | .redirectInput(ProcessBuilder.Redirect.INHERIT) 94 | .start(); 95 | try { 96 | return process.waitFor(); 97 | } catch (InterruptedException e) { 98 | throw new RuntimeException(e); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Resolve.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | import build.buildbuddy.Repository; 6 | import build.buildbuddy.Resolver; 7 | import build.buildbuddy.SequencedProperties; 8 | 9 | import java.io.IOException; 10 | import java.util.Map; 11 | import java.util.Objects; 12 | import java.util.Properties; 13 | import java.util.SequencedMap; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.CompletionStage; 16 | import java.util.concurrent.Executor; 17 | 18 | import static java.util.Objects.requireNonNull; 19 | 20 | public class Resolve implements DependencyTransformingBuildStep { 21 | 22 | private final Map repositories; 23 | private final Map resolvers; 24 | 25 | public Resolve(Map repositories, Map resolvers) { 26 | this.repositories = repositories; 27 | this.resolvers = resolvers; 28 | } 29 | 30 | @Override 31 | public CompletionStage transform(Executor executor, 32 | BuildStepContext context, 33 | SequencedMap arguments, 34 | SequencedMap> groups) 35 | throws IOException { 36 | Properties properties = new SequencedProperties(); 37 | for (Map.Entry> group : groups.entrySet()) { 38 | for (Map.Entry entry : requireNonNull( 39 | resolvers.get(group.getKey()), 40 | "Unknown resolver: " + group.getKey()).dependencies( 41 | executor, 42 | group.getKey(), 43 | repositories, 44 | group.getValue().sequencedKeySet()).entrySet()) { 45 | String value; 46 | if (Objects.equals(group.getKey(), entry.getKey().substring(0, entry.getKey().indexOf('/')))) { 47 | value = group.getValue().getOrDefault( 48 | entry.getKey().substring(entry.getKey().indexOf('/') + 1), 49 | entry.getValue()); 50 | } else { 51 | value = entry.getValue(); 52 | } 53 | properties.setProperty(entry.getKey(), value); 54 | } 55 | } 56 | return CompletableFuture.completedStage(properties); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/TestEngine.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.DirectoryStream; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.jar.Attributes; 12 | import java.util.jar.JarFile; 13 | import java.util.jar.Manifest; 14 | 15 | public enum TestEngine { 16 | 17 | JUNIT4("junit", "org.junit.runner.JUnitCore", ""), 18 | JUNIT5("org.junit.platform.console", 19 | "org.junit.platform.console.ConsoleLauncher", 20 | "-select-class=", 21 | "execute", 22 | "--disable-banner", 23 | "--disable-ansi-colors"); 24 | 25 | final String module, mainClass, prefix; 26 | final List arguments; 27 | 28 | TestEngine(String module, String mainClass, String prefix, String... arguments) { 29 | this.module = module; 30 | this.mainClass = mainClass; 31 | this.prefix = prefix; 32 | this.arguments = List.of(arguments); 33 | } 34 | 35 | public static Optional of(Iterable folders) throws IOException { 36 | TestEngine engine = null; 37 | for (Path folder : folders) { 38 | Path artifacts = folder.resolve(BuildStep.ARTIFACTS); 39 | if (Files.exists(artifacts)) { 40 | try (DirectoryStream stream = Files.newDirectoryStream(artifacts)) { 41 | for (Path file : stream) { 42 | try (JarFile jarFile = new JarFile(file.toFile())) { 43 | Manifest manifest = jarFile.getManifest(); 44 | if (manifest != null) { 45 | TestEngine candidate = switch (manifest 46 | .getMainAttributes() 47 | .getValue(Attributes.Name.IMPLEMENTATION_TITLE)) { 48 | case "JUnit" -> TestEngine.JUNIT4; 49 | case "junit-platform-console" -> TestEngine.JUNIT5; 50 | case null, default -> null; 51 | }; 52 | if (candidate != null) { 53 | if (engine == null || candidate.ordinal() > engine.ordinal()) { 54 | engine = candidate; 55 | } 56 | } 57 | } 58 | } catch (IOException e) { 59 | throw e; 60 | } catch (Exception _) { 61 | } 62 | } 63 | } 64 | } 65 | } 66 | return Optional.ofNullable(engine); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Tests.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.FileVisitResult; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.SimpleFileVisitor; 11 | import java.nio.file.attribute.BasicFileAttributes; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.SequencedMap; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.CompletionStage; 17 | import java.util.concurrent.Executor; 18 | import java.util.function.Function; 19 | import java.util.function.Predicate; 20 | import java.util.regex.Pattern; 21 | import java.util.stream.Stream; 22 | 23 | public class Tests extends Java { 24 | 25 | private final TestEngine engine; 26 | private final Predicate isTest; 27 | 28 | { 29 | jarsOnly = true; 30 | } 31 | 32 | public Tests() { 33 | this(null); 34 | } 35 | 36 | public Tests(TestEngine engine) { 37 | this.engine = engine; 38 | List patterns = Stream.of(".*\\.Test[a-zA-Z0-9$]*", ".*\\..*Test", ".*\\..*Tests", ".*\\..*TestCase") 39 | .map(Pattern::compile) 40 | .toList(); 41 | this.isTest = name -> patterns.stream().anyMatch(pattern -> pattern.matcher(name).matches()); 42 | } 43 | 44 | public Tests(TestEngine engine, Predicate isTest) { 45 | this.engine = engine; 46 | this.isTest = isTest; 47 | } 48 | 49 | public Tests(Function, ProcessHandler.OfProcess> factory, 50 | TestEngine engine, 51 | Predicate isTest) { 52 | super(factory); 53 | this.engine = engine; 54 | this.isTest = isTest; 55 | } 56 | 57 | @Override 58 | protected CompletionStage> commands(Executor executor, 59 | BuildStepContext context, 60 | SequencedMap arguments) 61 | throws IOException { 62 | TestEngine engine = this.engine == null ? TestEngine 63 | .of(() -> arguments.values().stream().map(BuildStepArgument::folder).iterator()) 64 | .orElseThrow(() -> new IllegalArgumentException("No test engine found")) : this.engine; 65 | List commands = new ArrayList<>(); 66 | if (modular && engine.module != null) { 67 | commands.add("--add-modules"); 68 | commands.add("ALL-MODULE-PATH"); 69 | commands.add("-m"); 70 | commands.add(engine.module + "/" + engine.mainClass); 71 | } else { 72 | commands.add(engine.mainClass); 73 | } 74 | commands.addAll(engine.arguments); 75 | for (BuildStepArgument argument : arguments.values()) { 76 | Path classes = argument.folder().resolve(CLASSES); 77 | if (Files.exists(classes)) { 78 | Files.walkFileTree(classes, new SimpleFileVisitor<>() { 79 | @Override 80 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 81 | if (file.toString().endsWith(".class")) { 82 | String raw = classes.relativize(file).toString(); 83 | String className = raw.substring(0, raw.length() - 6).replace('/', '.'); 84 | if (isTest.test(className)) { 85 | commands.add(engine.prefix + className); 86 | } 87 | } 88 | return FileVisitResult.CONTINUE; 89 | } 90 | }); 91 | } 92 | } 93 | return CompletableFuture.completedFuture(commands); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /sources/build/buildbuddy/step/Translate.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | import build.buildbuddy.SequencedProperties; 6 | 7 | import java.io.IOException; 8 | import java.util.Map; 9 | import java.util.Properties; 10 | import java.util.SequencedMap; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.CompletionStage; 13 | import java.util.concurrent.Executor; 14 | import java.util.function.Function; 15 | 16 | public class Translate implements DependencyTransformingBuildStep { 17 | 18 | private final Map> translators; 19 | 20 | public Translate(Map> translators) { 21 | this.translators = translators; 22 | } 23 | 24 | @Override 25 | public CompletionStage transform(Executor executor, 26 | BuildStepContext context, 27 | SequencedMap arguments, 28 | SequencedMap> groups) 29 | throws IOException { 30 | Properties properties = new SequencedProperties(); 31 | for (Map.Entry> group : groups.entrySet()) { 32 | Function translator = translators.get(group.getKey()); 33 | if (translator == null) { 34 | group.getValue().forEach((coordinate, expectation) -> properties.setProperty( 35 | group.getKey() + "/" + coordinate, 36 | expectation)); 37 | } else { 38 | group.getValue().forEach((coordinate, expectation) -> properties.setProperty( 39 | translator.apply(coordinate), 40 | expectation)); 41 | } 42 | } 43 | return CompletableFuture.completedStage(properties); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sources/module-info.java: -------------------------------------------------------------------------------- 1 | module buildbuddy { 2 | 3 | requires jdk.compiler; 4 | requires java.xml; 5 | 6 | exports build.buildbuddy; 7 | exports build.buildbuddy.maven; 8 | exports build.buildbuddy.module; 9 | exports build.buildbuddy.project; 10 | exports build.buildbuddy.step; 11 | } -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/BuildExecutorCallbackTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test; 2 | 3 | import build.buildbuddy.BuildExecutorCallback; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.PrintStream; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.LinkedHashSet; 10 | import java.util.Set; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class BuildExecutorCallbackTest { 15 | 16 | @Test 17 | public void can_print_executed() { 18 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 19 | try (PrintStream printStream = new PrintStream(outputStream)) { 20 | BuildExecutorCallback.printing(printStream) 21 | .step("foo", new LinkedHashSet<>(Set.of("bar"))) 22 | .accept(true, null); 23 | } 24 | assertThat(outputStream.toString(StandardCharsets.UTF_8)) 25 | .matches("\\[EXECUTED] foo in [0-9]+.[0-9]{2} seconds\n"); 26 | } 27 | 28 | @Test 29 | public void can_print_skipped() { 30 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 31 | try (PrintStream printStream = new PrintStream(outputStream)) { 32 | BuildExecutorCallback.printing(printStream) 33 | .step("foo", new LinkedHashSet<>(Set.of("bar"))) 34 | .accept(false, null); 35 | } 36 | assertThat(outputStream.toString(StandardCharsets.UTF_8)).matches("\\[SKIPPED] foo\n"); 37 | } 38 | 39 | @Test 40 | public void can_print_failed() { 41 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 42 | try (PrintStream printStream = new PrintStream(outputStream)) { 43 | BuildExecutorCallback.printing(printStream) 44 | .step("foo", new LinkedHashSet<>(Set.of("bar"))) 45 | .accept(null, new RuntimeException("message")); 46 | } 47 | assertThat(outputStream.toString(StandardCharsets.UTF_8)).matches("\\[FAILED] foo: message\n"); 48 | } 49 | } -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/ChecksumStatusTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test; 2 | 3 | import build.buildbuddy.ChecksumStatus; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.nio.file.Path; 7 | import java.util.Map; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class ChecksumStatusTest { 12 | 13 | @Test 14 | public void can_find_added() { 15 | Map status = ChecksumStatus.diff( 16 | Map.of(), 17 | Map.of(Path.of("foo"), new byte[]{1, 2, 3})); 18 | assertThat(status).containsOnly(Map.entry(Path.of("foo"), ChecksumStatus.ADDED)); 19 | } 20 | 21 | @Test 22 | public void can_find_removed() { 23 | Map status = ChecksumStatus.diff( 24 | Map.of(Path.of("foo"), new byte[]{1, 2, 3}), 25 | Map.of()); 26 | assertThat(status).containsOnly(Map.entry(Path.of("foo"), ChecksumStatus.REMOVED)); 27 | } 28 | 29 | @Test 30 | public void can_find_retained() { 31 | Map status = ChecksumStatus.diff( 32 | Map.of(Path.of("foo"), new byte[]{1, 2, 3}), 33 | Map.of(Path.of("foo"), new byte[]{1, 2, 3})); 34 | assertThat(status).containsOnly(Map.entry(Path.of("foo"), ChecksumStatus.RETAINED)); 35 | } 36 | 37 | @Test 38 | public void can_find_altered() { 39 | Map status = ChecksumStatus.diff( 40 | Map.of(Path.of("foo"), new byte[]{1, 2, 3}), 41 | Map.of(Path.of("foo"), new byte[]{4, 5, 6})); 42 | assertThat(status).containsOnly(Map.entry(Path.of("foo"), ChecksumStatus.ALTERED)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/HashDigestFunctionTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test; 2 | 3 | import build.buildbuddy.HashDigestFunction; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.io.TempDir; 6 | 7 | import java.io.IOException; 8 | import java.nio.charset.StandardCharsets; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.security.MessageDigest; 12 | import java.security.NoSuchAlgorithmException; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | public class HashDigestFunctionTest { 17 | 18 | @TempDir 19 | private Path files; 20 | 21 | @Test 22 | public void can_compute_hash() throws IOException, NoSuchAlgorithmException { 23 | Path file = Files.writeString(files.resolve("file"), "bar"); 24 | byte[] hash = new HashDigestFunction("MD5").hash(file); 25 | assertThat(hash).isEqualTo(MessageDigest.getInstance("MD5").digest("bar".getBytes(StandardCharsets.UTF_8))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/HashFunctionTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test; 2 | 3 | import build.buildbuddy.HashFunction; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.io.TempDir; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.Map; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class HashFunctionTest { 15 | 16 | @TempDir 17 | private Path folder; 18 | 19 | @Test 20 | public void can_write_file_and_read_file() throws IOException { 21 | Path file = folder.resolve("foo"); 22 | HashFunction.write(file, Map.of(Path.of("foo"), new byte[]{1, 2, 3})); 23 | Map checksums = HashFunction.read(file); 24 | assertThat(checksums).containsOnlyKeys(Path.of("foo")); 25 | assertThat(checksums.get(Path.of("foo"))).isEqualTo(new byte[]{1, 2, 3}); 26 | } 27 | 28 | @Test 29 | public void can_extract_folder() throws IOException { 30 | Files.writeString(folder.resolve("foo"), "bar"); 31 | Map checksums = HashFunction.read(folder, _ -> new byte[]{1, 2, 3}); 32 | assertThat(checksums).containsOnlyKeys(Path.of("foo")); 33 | assertThat(checksums.get(Path.of("foo"))).isEqualTo(new byte[]{1, 2, 3}); 34 | } 35 | 36 | @Test 37 | public void can_extract_nested_folder() throws IOException { 38 | Files.writeString(Files.createDirectory(folder.resolve("bar")).resolve("foo"), "bar"); 39 | Map checksums = HashFunction.read(folder, _ -> new byte[]{1, 2, 3}); 40 | assertThat(checksums).containsOnlyKeys(Path.of("bar/foo")); 41 | assertThat(checksums.get(Path.of("bar/foo"))).isEqualTo(new byte[]{1, 2, 3}); 42 | } 43 | 44 | @Test 45 | public void can_extract_empty_folder() throws IOException { 46 | Map checksums = HashFunction.read(folder, _ -> { 47 | throw new UnsupportedOperationException(); 48 | }); 49 | assertThat(checksums).isEmpty(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/SequencedPropertiesTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test; 2 | 3 | import build.buildbuddy.SequencedProperties; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.IOException; 7 | import java.io.StringReader; 8 | import java.io.StringWriter; 9 | import java.util.Properties; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.IntStream; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | public class SequencedPropertiesTest { 16 | 17 | @Test 18 | public void can_suppress_comments_and_subsequent_newline() throws IOException { 19 | Properties original = new SequencedProperties(); 20 | for (char character = 'z'; character >= 'a'; character--) { 21 | original.setProperty("key-" + character, "value-" + character); 22 | } 23 | StringWriter writer = new StringWriter(); 24 | original.store(writer, null); 25 | assertThat(writer.toString()).isEqualTo(IntStream.iterate('z', 26 | character -> character >= 'a', 27 | character -> character - 1) 28 | .mapToObj(character -> "key-" + (char) character + "=value-" + (char) character) 29 | .collect(Collectors.joining("\n", "", "\n"))); 30 | Properties copy = new SequencedProperties(); 31 | copy.load(new StringReader(writer.toString())); 32 | assertThat(copy.stringPropertyNames()).containsExactlyElementsOf(original.stringPropertyNames()); 33 | } 34 | } -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/maven/MavenPomEmitterTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.maven; 2 | 3 | import build.buildbuddy.maven.MavenDependencyKey; 4 | import build.buildbuddy.maven.MavenDependencyName; 5 | import build.buildbuddy.maven.MavenDependencyScope; 6 | import build.buildbuddy.maven.MavenDependencyValue; 7 | import build.buildbuddy.maven.MavenPomEmitter; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.io.IOException; 11 | import java.io.StringWriter; 12 | import java.nio.file.Path; 13 | import java.util.LinkedHashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | public class MavenPomEmitterTest { 20 | 21 | @Test 22 | public void can_emit_pom_with_defaults() throws IOException { 23 | StringWriter writer = new StringWriter(); 24 | new MavenPomEmitter().emit("group", 25 | "artifact", 26 | "version", 27 | null, 28 | new LinkedHashMap<>(Map.of( 29 | new MavenDependencyKey("other", "artifact", "jar", null), 30 | new MavenDependencyValue("version", 31 | MavenDependencyScope.COMPILE, 32 | null, 33 | null, 34 | null)))).accept(writer); 35 | assertThat(writer.toString()).isEqualTo(""" 36 | 37 | 38 | 4.0.0 39 | group 40 | artifact 41 | version 42 | 43 | 44 | other 45 | artifact 46 | version 47 | 48 | 49 | 50 | """); 51 | } 52 | 53 | @Test 54 | public void can_emit_pom() throws IOException { 55 | StringWriter writer = new StringWriter(); 56 | new MavenPomEmitter().emit("group", 57 | "artifact", 58 | "version", 59 | null, 60 | new LinkedHashMap<>(Map.of( 61 | new MavenDependencyKey("other", "artifact", "test-jar", "classifier"), 62 | new MavenDependencyValue("version", 63 | MavenDependencyScope.SYSTEM, 64 | Path.of("file.jar"), 65 | List.of(new MavenDependencyName("group", "artifact")), 66 | false)))).accept(writer); 67 | assertThat(writer.toString()).isEqualTo(""" 68 | 69 | 70 | 4.0.0 71 | group 72 | artifact 73 | version 74 | 75 | 76 | other 77 | artifact 78 | version 79 | test-jar 80 | classifier 81 | system 82 | file.jar 83 | false 84 | 85 | 86 | group 87 | artifact 88 | 89 | 90 | 91 | 92 | 93 | """); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/maven/MavenUriParserTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.maven; 2 | 3 | import build.buildbuddy.maven.MavenUriParser; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class MavenUriParserTest { 9 | 10 | @Test 11 | public void can_resolve_module() { 12 | assertThat(new MavenUriParser().apply( 13 | "https://host.org/maven2/foo/bar/qux/1/qux-1.jar")).isEqualTo("foo.bar/qux/1"); 14 | } 15 | @Test 16 | public void can_resolve_module_with_type() { 17 | assertThat(new MavenUriParser().apply( 18 | "https://host.org/maven2/foo/bar/qux/1/qux-1.zip")).isEqualTo("foo.bar/qux/zip/1"); 19 | } 20 | 21 | @Test 22 | public void can_resolve_module_with_classifier() { 23 | assertThat(new MavenUriParser().apply( 24 | "https://host.org/maven2/foo/bar/qux/1/qux-baz-1.jar")).isEqualTo("foo.bar/qux/jar/baz/1"); 25 | } 26 | } -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/module/ModularJarResolverTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.module; 2 | 3 | import build.buildbuddy.RepositoryItem; 4 | import build.buildbuddy.module.ModularJarResolver; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.lang.classfile.ClassFile; 12 | import java.lang.classfile.attribute.ModuleAttribute; 13 | import java.lang.classfile.attribute.ModuleRequireInfo; 14 | import java.lang.constant.ModuleDesc; 15 | import java.util.LinkedHashSet; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import java.util.SequencedMap; 19 | import java.util.Set; 20 | import java.util.jar.JarEntry; 21 | import java.util.jar.JarOutputStream; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | 25 | public class ModularJarResolverTest { 26 | 27 | @Test 28 | public void can_parse_module_info() throws IOException { 29 | SequencedMap dependencies = new ModularJarResolver(false).dependencies( 30 | Runnable::run, 31 | "foo", 32 | Map.of("foo", (_, coordinate) -> { 33 | RepositoryItem item = switch (coordinate) { 34 | case "root" -> () -> toJar("sample", "transitive"); 35 | case "transitive" -> () -> toJar("transitive", "last"); 36 | case "last" -> () -> toJar("last"); 37 | default -> null; 38 | }; 39 | return Optional.ofNullable(item); 40 | }), 41 | new LinkedHashSet<>(Set.of("root"))); 42 | assertThat(dependencies).containsExactly( 43 | Map.entry("foo/root", ""), 44 | Map.entry("foo/transitive", ""), 45 | Map.entry("foo/last", "")); 46 | } 47 | 48 | private static InputStream toJar(String module, String... requires) throws IOException { 49 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 50 | try (JarOutputStream jarOutputStream = new JarOutputStream(outputStream)) { 51 | jarOutputStream.putNextEntry(new JarEntry("module-info.class")); 52 | jarOutputStream.write(ClassFile.of().buildModule(ModuleAttribute.of( 53 | ModuleDesc.of(module), 54 | builder -> { 55 | builder.requires(ModuleRequireInfo.of(ModuleDesc.of("java.base"), 0, null)); 56 | for (String require : requires) { 57 | builder.requires(ModuleRequireInfo.of(ModuleDesc.of(require), 0, null)); 58 | } 59 | }))); 60 | jarOutputStream.closeEntry(); 61 | } 62 | return new ByteArrayInputStream(outputStream.toByteArray()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/module/ModuleInfoParserTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.module; 2 | 3 | import build.buildbuddy.module.ModuleInfo; 4 | import build.buildbuddy.module.ModuleInfoParser; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.io.TempDir; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.util.LinkedHashSet; 12 | import java.util.List; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | public class ModuleInfoParserTest { 17 | 18 | @TempDir 19 | private Path folder; 20 | 21 | @Test 22 | public void can_identify_module_info() throws IOException { 23 | Files.writeString(folder.resolve("module-info.java"), """ 24 | module foo { 25 | requires bar; 26 | opens qux; 27 | exports baz; 28 | } 29 | """); 30 | assertThat(new ModuleInfoParser().identify(folder.resolve("module-info.java"))).isEqualTo( 31 | new ModuleInfo("foo", new LinkedHashSet<>(List.of("bar")))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/project/DependenciesModuleTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.project; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.BuildExecutorCallback; 5 | import build.buildbuddy.BuildStep; 6 | import build.buildbuddy.HashDigestFunction; 7 | import build.buildbuddy.Resolver; 8 | import build.buildbuddy.project.DependenciesModule; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | 13 | import java.io.ByteArrayInputStream; 14 | import java.io.IOException; 15 | import java.io.Reader; 16 | import java.io.Writer; 17 | import java.nio.charset.StandardCharsets; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.security.MessageDigest; 21 | import java.security.NoSuchAlgorithmException; 22 | import java.util.HexFormat; 23 | import java.util.Map; 24 | import java.util.Optional; 25 | import java.util.Properties; 26 | import java.util.SequencedMap; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | public class DependenciesModuleTest { 31 | 32 | @TempDir 33 | private Path input, root; 34 | private BuildExecutor buildExecutor; 35 | 36 | @BeforeEach 37 | public void setUp() throws Exception { 38 | buildExecutor = BuildExecutor.of(root, 39 | new HashDigestFunction("MD5"), 40 | BuildExecutorCallback.nop()); 41 | } 42 | 43 | @Test 44 | public void can_resolve_dependencies() throws IOException { 45 | Properties dependencies = new Properties(); 46 | dependencies.setProperty("foo/bar", ""); 47 | try (Writer writer = Files.newBufferedWriter(input.resolve(BuildStep.DEPENDENCIES))) { 48 | dependencies.store(writer, null); 49 | } 50 | buildExecutor.addSource("input", input); 51 | buildExecutor.addModule("output", new DependenciesModule( 52 | Map.of("foo", (_, coordinate) -> Optional.of(() -> new ByteArrayInputStream( 53 | coordinate.getBytes(StandardCharsets.UTF_8)))), 54 | Map.of("foo", Resolver.identity())), "input"); 55 | SequencedMap steps = buildExecutor.execute(); 56 | assertThat(steps).containsKeys("output/resolved", "output/artifacts"); 57 | Properties resolved = new Properties(); 58 | try (Reader reader = Files.newBufferedReader(steps.get("output/resolved").resolve(BuildStep.DEPENDENCIES))) { 59 | resolved.load(reader); 60 | } 61 | assertThat(resolved.stringPropertyNames()).containsExactly("foo/bar"); 62 | assertThat(resolved.getProperty("foo/bar")).isEqualTo(""); 63 | assertThat(steps.get("output/artifacts") 64 | .resolve(BuildStep.ARTIFACTS) 65 | .resolve("foo-bar.jar")).content().isEqualTo("bar"); 66 | } 67 | 68 | @Test 69 | public void can_resolve_dependencies_with_checksums() throws IOException, NoSuchAlgorithmException { 70 | Properties dependencies = new Properties(); 71 | dependencies.setProperty("foo/bar", ""); 72 | try (Writer writer = Files.newBufferedWriter(input.resolve(BuildStep.DEPENDENCIES))) { 73 | dependencies.store(writer, null); 74 | } 75 | buildExecutor.addSource("input", input); 76 | buildExecutor.addModule("output", new DependenciesModule( 77 | Map.of("foo", (_, coordinate) -> Optional.of(() -> new ByteArrayInputStream( 78 | coordinate.getBytes(StandardCharsets.UTF_8)))), 79 | Map.of("foo", Resolver.identity())).computeChecksums("SHA256"), "input"); 80 | SequencedMap steps = buildExecutor.execute(); 81 | assertThat(steps).containsKeys("output/prepared", "output/resolved", "output/artifacts"); 82 | Properties resolved = new Properties(); 83 | try (Reader reader = Files.newBufferedReader(steps.get("output/resolved").resolve(BuildStep.DEPENDENCIES))) { 84 | resolved.load(reader); 85 | } 86 | assertThat(resolved.stringPropertyNames()).containsExactly("foo/bar"); 87 | assertThat(resolved.getProperty("foo/bar")).isEqualTo("SHA256/" + HexFormat.of().formatHex(MessageDigest 88 | .getInstance("SHA256") 89 | .digest("bar".getBytes(StandardCharsets.UTF_8)))); 90 | assertThat(steps.get("output/artifacts") 91 | .resolve(BuildStep.ARTIFACTS) 92 | .resolve("foo-bar.jar")).content().isEqualTo("bar"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/project/JavaModuleTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.project; 2 | 3 | import build.buildbuddy.BuildExecutor; 4 | import build.buildbuddy.BuildExecutorCallback; 5 | import build.buildbuddy.BuildStep; 6 | import build.buildbuddy.HashDigestFunction; 7 | import build.buildbuddy.project.JavaModule; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.io.TempDir; 11 | import sample.Sample; 12 | 13 | import java.io.BufferedWriter; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.net.URLEncoder; 18 | import java.nio.charset.StandardCharsets; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.SequencedMap; 25 | import java.util.UUID; 26 | import java.util.jar.JarEntry; 27 | import java.util.jar.JarInputStream; 28 | import java.util.jar.JarOutputStream; 29 | import java.util.zip.ZipEntry; 30 | 31 | import static java.util.Objects.requireNonNull; 32 | import static org.assertj.core.api.Assertions.assertThat; 33 | 34 | public class JavaModuleTest { 35 | 36 | @TempDir 37 | private Path input, root; 38 | private BuildExecutor buildExecutor; 39 | 40 | @BeforeEach 41 | public void setUp() throws Exception { 42 | buildExecutor = BuildExecutor.of(root, 43 | new HashDigestFunction("MD5"), 44 | BuildExecutorCallback.nop()); 45 | } 46 | 47 | @Test 48 | public void can_build_java() throws IOException { 49 | Path sources = Files.createDirectories(input.resolve(BuildStep.SOURCES + "other")); 50 | try (BufferedWriter writer = Files.newBufferedWriter(sources.resolve("Sample.java"))) { 51 | writer.append("package other;"); 52 | writer.newLine(); 53 | writer.append("public class Sample {"); 54 | writer.newLine(); 55 | writer.append(" sample.Sample s = new sample.Sample();"); 56 | writer.newLine(); 57 | writer.append("}"); 58 | writer.newLine(); 59 | } 60 | try (JarOutputStream outputStream = new JarOutputStream(Files.newOutputStream(Files 61 | .createDirectories(input.resolve(BuildStep.ARTIFACTS)) 62 | .resolve("dependency.jar"))); 63 | InputStream inputStream = requireNonNull(Sample.class.getResourceAsStream("Sample.class"))) { 64 | outputStream.putNextEntry(new JarEntry("sample/Sample.class")); 65 | inputStream.transferTo(outputStream); 66 | } 67 | buildExecutor.addSource("input", input); 68 | buildExecutor.addModule("output", new JavaModule(), "input"); 69 | SequencedMap steps = buildExecutor.execute(); 70 | assertThat(steps).containsKeys("output/classes", "output/artifacts"); 71 | assertThat(steps.get("output/classes").resolve(BuildStep.CLASSES).resolve("other/Sample.class")).exists(); 72 | try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(steps.get("output/artifacts") 73 | .resolve(BuildStep.ARTIFACTS) 74 | .resolve("classes.jar")))) { 75 | assertThat(inputStream.getNextJarEntry()) 76 | .extracting(ZipEntry::getName) 77 | .isEqualTo("other/"); 78 | assertThat(inputStream.getNextJarEntry()) 79 | .extracting(ZipEntry::getName) 80 | .isEqualTo("other/Sample.class"); 81 | assertThat(inputStream.getNextJarEntry()).isNull(); 82 | } 83 | } 84 | 85 | @Test 86 | public void can_build_java_with_junit() throws Exception { 87 | Path sources = Files.createDirectories(input.resolve(BuildStep.SOURCES + "other")); 88 | try (BufferedWriter writer = Files.newBufferedWriter(sources.resolve("SampleTest.java"))) { 89 | writer.append("package other;"); 90 | writer.newLine(); 91 | writer.append("public class SampleTest {"); 92 | writer.newLine(); 93 | writer.append(" @org.junit.jupiter.api.Test"); 94 | writer.newLine(); 95 | writer.append(" public void test() {"); 96 | writer.newLine(); 97 | writer.append(" System.out.println(\"Hello world!\");"); 98 | writer.newLine(); 99 | writer.append(" }"); 100 | writer.newLine(); 101 | writer.append("}"); 102 | writer.newLine(); 103 | } 104 | Path artifacts = Files.createDirectory(input.resolve(BuildStep.ARTIFACTS)); 105 | List elements = new ArrayList<>(); 106 | elements.addAll(Arrays.asList(System.getProperty("java.class.path", "").split(File.pathSeparator))); 107 | elements.addAll(Arrays.asList(System.getProperty("jdk.module.path", "").split(File.pathSeparator))); 108 | for (String element : elements) { 109 | if (element.endsWith("_rt.jar") || element.endsWith("-rt.jar")) { 110 | continue; 111 | } 112 | Path path = Path.of(element); 113 | if (Files.isRegularFile(path)) { 114 | Files.copy(path, artifacts.resolve(URLEncoder.encode( 115 | UUID.randomUUID().toString(), 116 | StandardCharsets.UTF_8) + ".jar")); 117 | } 118 | } 119 | buildExecutor.addSource("input", input); 120 | buildExecutor.addModule("output", new JavaModule().testIfAvailable(), "input"); 121 | SequencedMap steps = buildExecutor.execute(); 122 | assertThat(steps).containsKeys("output/classes", "output/artifacts", "output/tests"); 123 | assertThat(steps.get("output/classes").resolve(BuildStep.CLASSES).resolve("other/SampleTest.class")).exists(); 124 | try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(steps.get("output/artifacts") 125 | .resolve(BuildStep.ARTIFACTS) 126 | .resolve("classes.jar")))) { 127 | assertThat(inputStream.getNextJarEntry()) 128 | .extracting(ZipEntry::getName) 129 | .isEqualTo("other/"); 130 | assertThat(inputStream.getNextJarEntry()) 131 | .extracting(ZipEntry::getName) 132 | .isEqualTo("other/SampleTest.class"); 133 | assertThat(inputStream.getNextJarEntry()).isNull(); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/project/MultiProjectDependenciesTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.project; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.project.MultiProjectDependencies; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | 13 | import java.io.IOException; 14 | import java.io.Reader; 15 | import java.io.Writer; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.security.MessageDigest; 19 | import java.security.NoSuchAlgorithmException; 20 | import java.util.HexFormat; 21 | import java.util.LinkedHashMap; 22 | import java.util.Map; 23 | import java.util.Properties; 24 | 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | public class MultiProjectDependenciesTest { 28 | 29 | @TempDir 30 | private Path root, target; 31 | private Path previous, next, supplement, module, dependency; 32 | 33 | @BeforeEach 34 | public void setUp() throws Exception { 35 | previous = root.resolve("previous"); 36 | next = Files.createDirectory(root.resolve("next")); 37 | supplement = Files.createDirectory(root.resolve("supplement")); 38 | module = Files.createDirectory(root.resolve("module")); 39 | dependency = Files.createDirectory(root.resolve("dependency")); 40 | } 41 | 42 | @Test 43 | public void can_assign_coordinate_target_dependencies() throws IOException, NoSuchAlgorithmException { 44 | Properties dependencies = new Properties(); 45 | dependencies.setProperty("baz", ""); 46 | try (Writer writer = Files.newBufferedWriter(module.resolve(BuildStep.DEPENDENCIES))) { 47 | dependencies.store(writer, null); 48 | } 49 | Path file = target.resolve("file"); 50 | Files.writeString(file, "qux"); 51 | Properties coordinates = new Properties(); 52 | coordinates.setProperty("baz", file.toString()); 53 | try (Writer writer = Files.newBufferedWriter(dependency.resolve(BuildStep.COORDINATES))) { 54 | coordinates.store(writer, null); 55 | } 56 | BuildStepResult result = new MultiProjectDependencies("SHA256", "foo"::equals).apply( 57 | Runnable::run, 58 | new BuildStepContext(previous, next, supplement), 59 | new LinkedHashMap<>(Map.of( 60 | "foo", new BuildStepArgument( 61 | module, 62 | Map.of(Path.of(BuildStep.DEPENDENCIES), ChecksumStatus.ADDED)), 63 | "bar", new BuildStepArgument( 64 | dependency, 65 | Map.of(Path.of(BuildStep.COORDINATES), ChecksumStatus.ADDED))))) 66 | .toCompletableFuture().join(); 67 | assertThat(result.next()).isTrue(); 68 | Properties properties = new Properties(); 69 | try (Reader reader = Files.newBufferedReader(next.resolve(BuildStep.DEPENDENCIES))) { 70 | properties.load(reader); 71 | } 72 | assertThat(properties.stringPropertyNames()).containsExactly("baz"); 73 | assertThat(properties.getProperty("baz")).isEqualTo("SHA256/" + HexFormat.of().formatHex(MessageDigest 74 | .getInstance("SHA256") 75 | .digest("qux".getBytes()))); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/AssignTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.SequencedProperties; 9 | import build.buildbuddy.step.Assign; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.io.TempDir; 13 | 14 | import java.io.IOException; 15 | import java.io.Reader; 16 | import java.io.Writer; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | import java.util.Properties; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | 25 | public class AssignTest { 26 | 27 | @TempDir 28 | private Path root; 29 | private Path previous, next, supplement, argument; 30 | 31 | @BeforeEach 32 | public void setUp() throws Exception { 33 | previous = root.resolve("previous"); 34 | next = Files.createDirectory(root.resolve("next")); 35 | supplement = Files.createDirectory(root.resolve("supplement")); 36 | argument = Files.createDirectory(root.resolve("argument")); 37 | } 38 | 39 | @Test 40 | public void can_assign_all_dependencies() throws IOException { 41 | Properties properties = new SequencedProperties(); 42 | properties.setProperty("foo", ""); 43 | properties.setProperty("bar", "qux"); 44 | try (Writer writer = Files.newBufferedWriter(argument.resolve(BuildStep.COORDINATES))) { 45 | properties.store(writer, null); 46 | } 47 | Files.writeString(Files.createDirectory(argument.resolve(BuildStep.ARTIFACTS)).resolve("artifact"), "baz"); 48 | BuildStepResult result = new Assign().apply(Runnable::run, 49 | new BuildStepContext(previous, next, supplement), 50 | new LinkedHashMap<>(Map.of("argument", new BuildStepArgument( 51 | argument, 52 | Map.of( 53 | Path.of(BuildStep.COORDINATES), ChecksumStatus.ADDED, 54 | Path.of(BuildStep.ARTIFACTS + "foo"), ChecksumStatus.ADDED))))) 55 | .toCompletableFuture() 56 | .join(); 57 | assertThat(result.next()).isTrue(); 58 | Properties coordinates = new SequencedProperties(); 59 | try (Reader reader = Files.newBufferedReader(next.resolve(BuildStep.COORDINATES))) { 60 | coordinates.load(reader); 61 | } 62 | assertThat(coordinates).containsExactly( 63 | Map.entry("foo", argument.resolve(BuildStep.ARTIFACTS).resolve("artifact").toString()), 64 | Map.entry("bar", "qux")); 65 | } 66 | 67 | @Test 68 | public void can_assign_dependencies() throws IOException { 69 | Properties properties = new SequencedProperties(); 70 | properties.setProperty("foo", ""); 71 | properties.setProperty("bar", "qux"); 72 | try (Writer writer = Files.newBufferedWriter(argument.resolve(BuildStep.COORDINATES))) { 73 | properties.store(writer, null); 74 | } 75 | Files.writeString(Files.createDirectory(argument.resolve(BuildStep.ARTIFACTS)).resolve("artifact"), "baz"); 76 | BuildStepResult result = new Assign((coordinates, files) -> { 77 | assertThat(coordinates).containsExactly("foo"); 78 | assertThat(files).containsExactly(argument.resolve(BuildStep.ARTIFACTS).resolve("artifact")); 79 | return Map.of( 80 | "foo", argument.resolve(BuildStep.ARTIFACTS).resolve("artifact"), 81 | "qux", argument.resolve(BuildStep.ARTIFACTS).resolve("artifact")); 82 | }).apply(Runnable::run, 83 | new BuildStepContext(previous, next, supplement), 84 | new LinkedHashMap<>(Map.of("argument", new BuildStepArgument( 85 | argument, 86 | Map.of( 87 | Path.of(BuildStep.COORDINATES), ChecksumStatus.ADDED, 88 | Path.of(BuildStep.ARTIFACTS + "foo"), ChecksumStatus.ADDED))))) 89 | .toCompletableFuture() 90 | .join(); 91 | assertThat(result.next()).isTrue(); 92 | Properties coordinates = new SequencedProperties(); 93 | try (Reader reader = Files.newBufferedReader(next.resolve(BuildStep.COORDINATES))) { 94 | coordinates.load(reader); 95 | } 96 | assertThat(coordinates).containsExactly( 97 | Map.entry("foo", argument.resolve(BuildStep.ARTIFACTS).resolve("artifact").toString()), 98 | Map.entry("bar", "qux"), 99 | Map.entry("qux", argument.resolve(BuildStep.ARTIFACTS).resolve("artifact").toString())); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/BindTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | import build.buildbuddy.BuildStepResult; 6 | import build.buildbuddy.ChecksumStatus; 7 | import build.buildbuddy.step.Bind; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.io.TempDir; 11 | 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.util.LinkedHashMap; 16 | import java.util.Map; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | public class BindTest { 21 | 22 | @TempDir 23 | private Path root; 24 | private Path previous, next, supplement, original; 25 | 26 | @BeforeEach 27 | public void setUp() throws Exception { 28 | previous = root.resolve("previous"); 29 | next = Files.createDirectory(root.resolve("next")); 30 | supplement = Files.createDirectory(root.resolve("supplement")); 31 | original = Files.createDirectory(root.resolve("original")); 32 | } 33 | 34 | @Test 35 | public void can_link_files() throws IOException { 36 | Files.writeString(original.resolve("file"), "foo"); 37 | Files.writeString(Files.createDirectories(original.resolve("folder/sub")).resolve("file"), "bar"); 38 | BuildStepResult result = new Bind( 39 | Map.of( 40 | Path.of("file"), Path.of("other/copied"), 41 | Path.of("folder"), Path.of("other"))).apply( 42 | Runnable::run, 43 | new BuildStepContext(previous, next, supplement), 44 | new LinkedHashMap<>(Map.of("original", new BuildStepArgument( 45 | original, 46 | Map.of(Path.of("file"), ChecksumStatus.ADDED, 47 | Path.of("folder/sub/file"), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 48 | assertThat(result.next()).isTrue(); 49 | assertThat(next.resolve("other/copied")).content().isEqualTo("foo"); 50 | assertThat(next.resolve("other/sub/file")).content().isEqualTo("bar"); 51 | } 52 | } -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/ChecksumTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.step.Checksum; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | 13 | import java.io.ByteArrayInputStream; 14 | import java.io.IOException; 15 | import java.io.Reader; 16 | import java.io.Writer; 17 | import java.nio.charset.StandardCharsets; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.security.MessageDigest; 21 | import java.security.NoSuchAlgorithmException; 22 | import java.util.HexFormat; 23 | import java.util.LinkedHashMap; 24 | import java.util.Map; 25 | import java.util.Optional; 26 | import java.util.Properties; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | public class ChecksumTest { 31 | 32 | @TempDir 33 | private Path root; 34 | private Path previous, next, supplement, dependencies; 35 | 36 | @BeforeEach 37 | public void setUp() throws Exception { 38 | previous = root.resolve("previous"); 39 | next = Files.createDirectory(root.resolve("next")); 40 | supplement = Files.createDirectory(root.resolve("supplement")); 41 | dependencies = Files.createDirectory(root.resolve("dependencies")); 42 | } 43 | 44 | @Test 45 | public void can_resolve_checksums() throws IOException, NoSuchAlgorithmException { 46 | Properties properties = new Properties(); 47 | properties.setProperty("foo/qux", ""); 48 | properties.setProperty("foo/baz", ""); 49 | try (Writer writer = Files.newBufferedWriter(dependencies.resolve(BuildStep.DEPENDENCIES))) { 50 | properties.store(writer, null); 51 | } 52 | BuildStepResult result = new Checksum("SHA256", Map.of( 53 | "foo", 54 | (_, coordinate) -> Optional.of(() -> new ByteArrayInputStream(coordinate.getBytes(StandardCharsets.UTF_8))))).apply( 55 | Runnable::run, 56 | new BuildStepContext(previous, next, supplement), 57 | new LinkedHashMap<>(Map.of("dependencies", new BuildStepArgument( 58 | dependencies, 59 | Map.of( 60 | Path.of(BuildStep.DEPENDENCIES), 61 | ChecksumStatus.ADDED))))).toCompletableFuture().join(); 62 | assertThat(result.next()).isTrue(); 63 | Properties dependencies = new Properties(); 64 | try (Reader reader = Files.newBufferedReader(next.resolve(BuildStep.DEPENDENCIES))) { 65 | dependencies.load(reader); 66 | } 67 | assertThat(dependencies.stringPropertyNames()).containsExactlyInAnyOrder("foo/qux", "foo/baz"); 68 | assertThat(dependencies.getProperty("foo/qux")).isEqualTo("SHA256/" + HexFormat.of().formatHex( 69 | MessageDigest.getInstance("SHA256").digest("qux".getBytes(StandardCharsets.UTF_8)))); 70 | assertThat(dependencies.getProperty("foo/baz")).isEqualTo("SHA256/" + HexFormat.of().formatHex( 71 | MessageDigest.getInstance("SHA256").digest("baz".getBytes(StandardCharsets.UTF_8)))); 72 | } 73 | 74 | @Test 75 | public void retains_predefined_checksum() throws IOException, NoSuchAlgorithmException { 76 | Properties properties = new Properties(); 77 | properties.setProperty("foo/qux", "baz"); 78 | properties.setProperty("foo/baz", ""); 79 | try (Writer writer = Files.newBufferedWriter(dependencies.resolve(BuildStep.DEPENDENCIES))) { 80 | properties.store(writer, null); 81 | } 82 | BuildStepResult result = new Checksum("SHA256", Map.of( 83 | "foo", 84 | (_, coordinate) -> Optional.of(() -> new ByteArrayInputStream(coordinate.getBytes(StandardCharsets.UTF_8))))).apply( 85 | Runnable::run, 86 | new BuildStepContext(previous, next, supplement), 87 | new LinkedHashMap<>(Map.of("dependencies", new BuildStepArgument( 88 | dependencies, 89 | Map.of( 90 | Path.of(BuildStep.DEPENDENCIES), 91 | ChecksumStatus.ADDED))))).toCompletableFuture().join(); 92 | assertThat(result.next()).isTrue(); 93 | Properties dependencies = new Properties(); 94 | try (Reader reader = Files.newBufferedReader(next.resolve(BuildStep.DEPENDENCIES))) { 95 | dependencies.load(reader); 96 | } 97 | assertThat(dependencies.stringPropertyNames()).containsExactlyInAnyOrder("foo/qux", "foo/baz"); 98 | assertThat(dependencies.getProperty("foo/qux")).isEqualTo("baz"); 99 | assertThat(dependencies.getProperty("foo/baz")).isEqualTo("SHA256/" + HexFormat.of().formatHex( 100 | MessageDigest.getInstance("SHA256").digest("baz".getBytes(StandardCharsets.UTF_8)))); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/GroupTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.step.Group; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | 13 | import java.io.IOException; 14 | import java.io.Reader; 15 | import java.io.Writer; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.LinkedHashMap; 19 | import java.util.Map; 20 | import java.util.Optional; 21 | import java.util.Properties; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | 25 | public class GroupTest { 26 | 27 | @TempDir 28 | private Path root; 29 | private Path previous, next, supplement, left, right; 30 | 31 | @BeforeEach 32 | public void setUp() throws Exception { 33 | previous = root.resolve("previous"); 34 | next = Files.createDirectory(root.resolve("next")); 35 | supplement = Files.createDirectory(root.resolve("supplement")); 36 | left = Files.createDirectory(root.resolve("left")); 37 | right = Files.createDirectory(root.resolve("right")); 38 | } 39 | 40 | @Test 41 | public void can_link_related_groups() throws IOException { 42 | Properties leftCoordinates = new Properties(), rightCoordinates = new Properties(); 43 | leftCoordinates.setProperty("foo", ""); 44 | rightCoordinates.setProperty("bar", ""); 45 | try (Writer writer = Files.newBufferedWriter(left.resolve(BuildStep.COORDINATES))) { 46 | leftCoordinates.store(writer, null); 47 | } 48 | try (Writer writer = Files.newBufferedWriter(right.resolve(BuildStep.COORDINATES))) { 49 | rightCoordinates.store(writer, null); 50 | } 51 | Properties leftDependencies = new Properties(), rightDependencies = new Properties(); 52 | leftDependencies.setProperty("qux", ""); 53 | rightDependencies.setProperty("foo", ""); 54 | rightDependencies.setProperty("baz", ""); 55 | try (Writer writer = Files.newBufferedWriter(left.resolve(BuildStep.DEPENDENCIES))) { 56 | leftDependencies.store(writer, null); 57 | } 58 | try (Writer writer = Files.newBufferedWriter(right.resolve(BuildStep.DEPENDENCIES))) { 59 | rightDependencies.store(writer, null); 60 | } 61 | BuildStepResult result = new Group(Optional::of).apply( 62 | Runnable::run, 63 | new BuildStepContext(previous, next, supplement), 64 | new LinkedHashMap<>(Map.of( 65 | "left", new BuildStepArgument( 66 | left, 67 | Map.of( 68 | Path.of(BuildStep.COORDINATES), ChecksumStatus.ADDED, 69 | Path.of(BuildStep.DEPENDENCIES), ChecksumStatus.ADDED)), 70 | "right", new BuildStepArgument( 71 | right, 72 | Map.of( 73 | Path.of(BuildStep.COORDINATES), ChecksumStatus.ADDED, 74 | Path.of(BuildStep.DEPENDENCIES), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 75 | Properties leftGroup = new Properties(), rightGroup = new Properties(); 76 | try (Reader reader = Files.newBufferedReader(next.resolve(Group.GROUPS + "left.properties"))) { 77 | leftGroup.load(reader); 78 | } 79 | try (Reader reader = Files.newBufferedReader(next.resolve(Group.GROUPS + "right.properties"))) { 80 | rightGroup.load(reader); 81 | } 82 | assertThat(leftGroup.stringPropertyNames()).isEmpty(); 83 | assertThat(rightGroup.stringPropertyNames()).containsExactly("left"); 84 | assertThat(result.next()).isTrue(); 85 | } 86 | 87 | @Test 88 | public void does_not_link_unrelated_groups() throws IOException { 89 | Properties leftCoordinates = new Properties(), rightCoordinates = new Properties(); 90 | leftCoordinates.setProperty("foo", ""); 91 | rightCoordinates.setProperty("bar", ""); 92 | try (Writer writer = Files.newBufferedWriter(left.resolve(BuildStep.COORDINATES))) { 93 | leftCoordinates.store(writer, null); 94 | } 95 | try (Writer writer = Files.newBufferedWriter(right.resolve(BuildStep.COORDINATES))) { 96 | rightCoordinates.store(writer, null); 97 | } 98 | Properties leftDependencies = new Properties(), rightDependencies = new Properties(); 99 | leftDependencies.setProperty("qux", ""); 100 | rightDependencies.setProperty("baz", ""); 101 | try (Writer writer = Files.newBufferedWriter(left.resolve(BuildStep.DEPENDENCIES))) { 102 | leftDependencies.store(writer, null); 103 | } 104 | try (Writer writer = Files.newBufferedWriter(right.resolve(BuildStep.DEPENDENCIES))) { 105 | rightDependencies.store(writer, null); 106 | } 107 | BuildStepResult result = new Group(Optional::of).apply( 108 | Runnable::run, 109 | new BuildStepContext(previous, next, supplement), 110 | new LinkedHashMap<>(Map.of( 111 | "left", new BuildStepArgument( 112 | left, 113 | Map.of( 114 | Path.of(BuildStep.COORDINATES), ChecksumStatus.ADDED, 115 | Path.of(BuildStep.DEPENDENCIES), ChecksumStatus.ADDED)), 116 | "right", new BuildStepArgument( 117 | right, 118 | Map.of( 119 | Path.of(BuildStep.COORDINATES), ChecksumStatus.ADDED, 120 | Path.of(BuildStep.DEPENDENCIES), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 121 | Properties leftGroup = new Properties(), rightGroup = new Properties(); 122 | try (Reader reader = Files.newBufferedReader(next.resolve(Group.GROUPS + "left.properties"))) { 123 | leftGroup.load(reader); 124 | } 125 | try (Reader reader = Files.newBufferedReader(next.resolve(Group.GROUPS + "right.properties"))) { 126 | rightGroup.load(reader); 127 | } 128 | assertThat(leftGroup.stringPropertyNames()).isEmpty(); 129 | assertThat(rightGroup.stringPropertyNames()).isEmpty(); 130 | assertThat(result.next()).isTrue(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/JarTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.step.Jar; 9 | import build.buildbuddy.step.Javac; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.io.TempDir; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | import sample.Sample; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.util.LinkedHashMap; 21 | import java.util.Map; 22 | 23 | import static java.util.Objects.requireNonNull; 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | 26 | public class JarTest { 27 | 28 | @TempDir 29 | private Path root; 30 | private Path previous, next, supplement, classes; 31 | 32 | @BeforeEach 33 | public void setUp() throws Exception { 34 | previous = root.resolve("previous"); 35 | next = Files.createDirectory(root.resolve("next")); 36 | supplement = Files.createDirectory(root.resolve("supplement")); 37 | classes = Files.createDirectory(root.resolve("classes")); 38 | } 39 | 40 | @ParameterizedTest 41 | @ValueSource(booleans = {true, false}) 42 | public void can_execute_jarl(boolean process) throws IOException { 43 | Path folder = Files.createDirectory(classes.resolve(Javac.CLASSES)); 44 | try (InputStream inputStream = Sample.class.getResourceAsStream(Sample.class.getSimpleName() + ".class")) { 45 | Files.copy(requireNonNull(inputStream), Files 46 | .createDirectory(folder.resolve("sample")) 47 | .resolve("Sample.class")); 48 | } 49 | BuildStepResult result = (process ? Jar.process() : Jar.tool()).apply( 50 | Runnable::run, 51 | new BuildStepContext(previous, next, supplement), 52 | new LinkedHashMap<>(Map.of("sources", new BuildStepArgument( 53 | classes, 54 | Map.of(Path.of("sample/Sample.class"), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 55 | assertThat(result.next()).isTrue(); 56 | assertThat(next.resolve(BuildStep.ARTIFACTS + "classes.jar")).isNotEmptyFile(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/JavaTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStepArgument; 4 | import build.buildbuddy.BuildStepContext; 5 | import build.buildbuddy.BuildStepResult; 6 | import build.buildbuddy.ChecksumStatus; 7 | import build.buildbuddy.step.Java; 8 | import build.buildbuddy.step.Javac; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | import sample.Sample; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.LinkedHashMap; 19 | import java.util.Map; 20 | 21 | import static java.util.Objects.requireNonNull; 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | public class JavaTest { 25 | 26 | @TempDir 27 | private Path root; 28 | private Path previous, next, supplement, classes; 29 | 30 | @BeforeEach 31 | public void setUp() throws Exception { 32 | previous = root.resolve("previous"); 33 | next = Files.createDirectory(root.resolve("next")); 34 | supplement = Files.createDirectory(root.resolve("supplement")); 35 | classes = Files.createDirectory(root.resolve("classes")); 36 | } 37 | 38 | @Test 39 | public void can_execute_java() throws IOException { 40 | Path folder = Files.createDirectories(classes.resolve(Javac.CLASSES + "sample")); 41 | try (InputStream input = Sample.class.getResourceAsStream(Sample.class.getSimpleName() + ".class")) { 42 | Files.copy(requireNonNull(input), folder.resolve("Sample.class")); 43 | } 44 | BuildStepResult result = Java.of("sample.Sample").apply( 45 | Runnable::run, 46 | new BuildStepContext(previous, next, supplement), 47 | new LinkedHashMap<>(Map.of("classes", new BuildStepArgument( 48 | classes, 49 | Map.of(Path.of("sample/Sample.class"), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 50 | assertThat(result.next()).isTrue(); 51 | assertThat(supplement.resolve("output")).content().isEqualTo("Hello world!"); 52 | assertThat(supplement.resolve("error")).isEmptyFile(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/JavacTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.step.Javac; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | 15 | import java.io.BufferedWriter; 16 | import java.io.IOException; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | public class JavacTest { 25 | 26 | @TempDir 27 | private Path root; 28 | private Path previous, next, supplement, sources; 29 | 30 | @BeforeEach 31 | public void setUp() throws Exception { 32 | previous = root.resolve("previous"); 33 | next = Files.createDirectory(root.resolve("next")); 34 | supplement = Files.createDirectory(root.resolve("supplement")); 35 | sources = Files.createDirectory(root.resolve("sources")); 36 | } 37 | 38 | @ParameterizedTest 39 | @ValueSource(booleans = {true, false}) 40 | public void can_execute_javac(boolean process) throws IOException { 41 | Path folder = Files.createDirectories(sources.resolve(BuildStep.SOURCES + "sample")); 42 | try (BufferedWriter writer = Files.newBufferedWriter(folder.resolve("Sample.java"))) { 43 | writer.append("package sample;"); 44 | writer.newLine(); 45 | writer.append("public class Sample { }"); 46 | writer.newLine(); 47 | } 48 | BuildStepResult result = (process ? Javac.process() : Javac.tool()).apply(Runnable::run, 49 | new BuildStepContext(previous, next, supplement), 50 | new LinkedHashMap<>(Map.of("sources", new BuildStepArgument( 51 | sources, 52 | Map.of(Path.of("sources/sample/Sample.java"), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 53 | assertThat(result.next()).isTrue(); 54 | assertThat(next.resolve(Javac.CLASSES + "sample/Sample.class")).isNotEmptyFile(); 55 | } 56 | 57 | @ParameterizedTest 58 | @ValueSource(booleans = {true, false}) 59 | public void can_execute_javac_with_resources(boolean process) throws IOException { 60 | Path folder = Files.createDirectories(sources.resolve(BuildStep.SOURCES + "sample")); 61 | try (BufferedWriter writer = Files.newBufferedWriter(folder.resolve("Sample.java"))) { 62 | writer.append("package sample;"); 63 | writer.newLine(); 64 | writer.append("public class Sample { }"); 65 | writer.newLine(); 66 | } 67 | Files.writeString(folder.resolve("foo"), "bar"); 68 | Files.createDirectory(sources.resolve(BuildStep.SOURCES + "folder")); 69 | BuildStepResult result = (process ? Javac.process() : Javac.tool()).apply(Runnable::run, 70 | new BuildStepContext(previous, next, supplement), 71 | new LinkedHashMap<>(Map.of("sources", new BuildStepArgument( 72 | sources, 73 | Map.of(Path.of("sources/sample/Sample.java"), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 74 | assertThat(result.next()).isTrue(); 75 | assertThat(next.resolve(Javac.CLASSES + "sample/Sample.class")).isNotEmptyFile(); 76 | assertThat(next.resolve(Javac.CLASSES + "sample/foo")).content().isEqualTo("bar"); 77 | assertThat(next.resolve(Javac.CLASSES + "folder")).isDirectory(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/TestsTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.step.Javac; 9 | import build.buildbuddy.step.Tests; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.io.TempDir; 13 | import sample.TestSample; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.UUID; 27 | import java.util.stream.Collectors; 28 | 29 | import static java.util.Objects.requireNonNull; 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | 32 | public class TestsTest { 33 | 34 | @TempDir 35 | private Path root; 36 | private List appended; 37 | private Path previous, next, supplement, dependencies, classes; 38 | 39 | @BeforeEach 40 | public void setUp() throws Exception { 41 | previous = root.resolve("previous"); 42 | next = Files.createDirectory(root.resolve("next")); 43 | supplement = Files.createDirectory(root.resolve("supplement")); 44 | dependencies = Files.createDirectories(root.resolve("dependencies")); 45 | classes = Files.createDirectories(root.resolve("classes")); 46 | Path artifacts = Files.createDirectory(dependencies.resolve(BuildStep.ARTIFACTS)); 47 | List elements = new ArrayList<>(); 48 | elements.addAll(Arrays.asList(System.getProperty("java.class.path", "").split(File.pathSeparator))); 49 | elements.addAll(Arrays.asList(System.getProperty("jdk.module.path", "").split(File.pathSeparator))); 50 | appended = new ArrayList<>(); 51 | for (String element : elements) { 52 | if (element.endsWith("_rt.jar") || element.endsWith("-rt.jar")) { 53 | continue; 54 | } 55 | Path path = Path.of(element); 56 | if (Files.isRegularFile(path)) { 57 | String name = path.getFileName().toFile() + "-" + UUID.randomUUID() + ".jar"; 58 | appended.add(name); 59 | Files.copy(path, artifacts.resolve(name)); 60 | } 61 | } 62 | try (InputStream input = TestSample.class.getResourceAsStream(TestSample.class.getSimpleName() + ".class"); 63 | OutputStream output = Files.newOutputStream(Files 64 | .createDirectories(classes.resolve(Javac.CLASSES + "sample")) 65 | .resolve("TestSample.class"))) { 66 | requireNonNull(input).transferTo(output); 67 | } 68 | } 69 | 70 | @Test 71 | public void can_execute_junit() throws IOException { 72 | BuildStepResult result = new Tests(null, candidate -> candidate.endsWith("TestSample")).jarsOnly(false).apply( 73 | Runnable::run, 74 | new BuildStepContext(previous, next, supplement), 75 | new LinkedHashMap<>(Map.of( 76 | "dependencies", new BuildStepArgument( 77 | dependencies, 78 | appended.stream().collect(Collectors.toMap( 79 | name -> Path.of(BuildStep.ARTIFACTS + name), 80 | _ -> ChecksumStatus.ADDED))), 81 | "classes", new BuildStepArgument( 82 | classes, 83 | Map.of(Path.of(Javac.CLASSES + "sample/TestSample.class"), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 84 | assertThat(result.next()).isTrue(); 85 | assertThat(supplement.resolve("output")).content().contains("Hello world!"); 86 | assertThat(supplement.resolve("error")).isEmptyFile(); 87 | } 88 | 89 | @Test 90 | public void can_execute_junit_non_modular() throws IOException { 91 | BuildStepResult result = new Tests(null, candidate -> candidate.endsWith("TestSample")).jarsOnly(false).modular(false).apply( 92 | Runnable::run, 93 | new BuildStepContext(previous, next, supplement), 94 | new LinkedHashMap<>(Map.of( 95 | "dependencies", new BuildStepArgument( 96 | dependencies, 97 | appended.stream().collect(Collectors.toMap( 98 | name -> Path.of(BuildStep.ARTIFACTS + name), 99 | _ -> ChecksumStatus.ADDED))), 100 | "classes", new BuildStepArgument( 101 | classes, 102 | Map.of(Path.of(Javac.CLASSES + "sample/TestSample.class"), ChecksumStatus.ADDED))))).toCompletableFuture().join(); 103 | assertThat(result.next()).isTrue(); 104 | assertThat(supplement.resolve("output")).content().contains("Hello world!"); 105 | assertThat(supplement.resolve("error")).isEmptyFile(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/build/buildbuddy/test/step/TranslateTest.java: -------------------------------------------------------------------------------- 1 | package build.buildbuddy.test.step; 2 | 3 | import build.buildbuddy.BuildStep; 4 | import build.buildbuddy.BuildStepArgument; 5 | import build.buildbuddy.BuildStepContext; 6 | import build.buildbuddy.BuildStepResult; 7 | import build.buildbuddy.ChecksumStatus; 8 | import build.buildbuddy.step.Translate; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | 13 | import java.io.IOException; 14 | import java.io.Reader; 15 | import java.io.Writer; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.LinkedHashMap; 19 | import java.util.Map; 20 | import java.util.Properties; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | public class TranslateTest { 25 | 26 | @TempDir 27 | private Path root; 28 | private Path previous, next, supplement, dependencies; 29 | 30 | @BeforeEach 31 | public void setUp() throws Exception { 32 | previous = root.resolve("previous"); 33 | next = Files.createDirectory(root.resolve("next")); 34 | supplement = Files.createDirectory(root.resolve("supplement")); 35 | dependencies = Files.createDirectory(root.resolve("dependencies")); 36 | } 37 | 38 | @Test 39 | public void can_translate_dependencies() throws IOException { 40 | Properties properties = new Properties(); 41 | properties.setProperty("foo/qux", "foobar"); 42 | properties.setProperty("bar/baz", "quxbaz"); 43 | try (Writer writer = Files.newBufferedWriter(dependencies.resolve(BuildStep.DEPENDENCIES))) { 44 | properties.store(writer, null); 45 | } 46 | BuildStepResult result = new Translate(Map.of( 47 | "foo", 48 | coordinate -> "translated/" + coordinate)).apply( 49 | Runnable::run, 50 | new BuildStepContext(previous, next, supplement), 51 | new LinkedHashMap<>(Map.of("dependencies", new BuildStepArgument( 52 | dependencies, 53 | Map.of( 54 | Path.of(BuildStep.DEPENDENCIES), 55 | ChecksumStatus.ADDED))))).toCompletableFuture().join(); 56 | assertThat(result.next()).isTrue(); 57 | Properties dependencies = new Properties(); 58 | try (Reader reader = Files.newBufferedReader(next.resolve(BuildStep.DEPENDENCIES))) { 59 | dependencies.load(reader); 60 | } 61 | assertThat(dependencies.stringPropertyNames()).containsExactlyInAnyOrder("translated/qux", "bar/baz"); 62 | assertThat(dependencies.getProperty("translated/qux")).isEqualTo("foobar"); 63 | assertThat(dependencies.getProperty("bar/baz")).isEqualTo("quxbaz"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/module-info.java: -------------------------------------------------------------------------------- 1 | open module buildbuddy.test { 2 | 3 | requires buildbuddy; 4 | requires org.junit.jupiter; 5 | requires org.assertj.core; 6 | 7 | requires org.apiguardian.api; 8 | requires static org.junit.platform.console; 9 | } 10 | -------------------------------------------------------------------------------- /tests/sample/Sample.java: -------------------------------------------------------------------------------- 1 | package sample; 2 | 3 | public class Sample { 4 | 5 | public static void main(String[] args) { 6 | System.out.print("Hello world!"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/sample/TestSample.java: -------------------------------------------------------------------------------- 1 | package sample; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class TestSample { 6 | 7 | @Test 8 | public void test() { 9 | System.out.println("Hello world!"); 10 | } 11 | } 12 | --------------------------------------------------------------------------------