tokenizeStrings() {
21 | return Map.ofEntries(
22 | Map.entry("echo", List.of("echo")),
23 | Map.entry("mysql -u root -p 'my database'", List.of("mysql", "-u", "root", "-p", "my database")),
24 | Map.entry("echo foo='bar'", List.of("echo", "foo=bar"))
25 | )
26 | .entrySet()
27 | .stream()
28 | .map(entry -> DynamicTest.dynamicTest(entry.getKey(), () -> {
29 | final var tokens = ShellTokenizer.tokenize(entry.getKey());
30 | assertIterableEquals(entry.getValue(), tokens);
31 | }));
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/architecture/LayerRules.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.architecture;
7 |
8 | import com.tngtech.archunit.junit.ArchTest;
9 | import com.tngtech.archunit.lang.ArchRule;
10 | import org.junit.jupiter.api.DisplayName;
11 |
12 | import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
13 |
14 | @DisplayName("Layer Rules")
15 | public final class LayerRules {
16 |
17 | @ArchTest
18 | public static final ArchRule layerAccessControl = layeredArchitecture()
19 | .consideringAllDependencies()
20 | .withOptionalLayers(true)
21 | .layer("CLI").definedBy("wtf.metio.ilo.cli..")
22 | .layer("Commands").definedBy("wtf.metio.ilo.shell..")
23 | .layer("Errors").definedBy("wtf.metio.ilo.errors..")
24 | .layer("Models").definedBy("wtf.metio.ilo.model..")
25 | .layer("Tools").definedBy("wtf.metio.ilo.tools..")
26 | .layer("Utils").definedBy("wtf.metio.ilo.utils..")
27 | .whereLayer("Models").mayOnlyBeAccessedByLayers("Commands", "CLI", "Tools");
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/errors/ExitCodesTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.errors;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import picocli.CommandLine;
11 |
12 | import static org.junit.jupiter.api.Assertions.assertEquals;
13 |
14 | @DisplayName("ExitCodes")
15 | class ExitCodesTest {
16 |
17 | @Test
18 | @DisplayName("handles business exceptions")
19 | void businessException() {
20 | final var exitCodes = new ExitCodes();
21 | final var exception = new NoMatchingRuntimeException();
22 |
23 | final var exitCode = exitCodes.getExitCode(exception);
24 |
25 | assertEquals(exception.getExitCode(), exitCode);
26 | }
27 |
28 | @Test
29 | @DisplayName("handles generic exceptions")
30 | void genericException() {
31 | final var exitCodes = new ExitCodes();
32 | final var exception = new RuntimeException();
33 |
34 | final var exitCode = exitCodes.getExitCode(exception);
35 |
36 | assertEquals(CommandLine.ExitCode.SOFTWARE, exitCode);
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/shell/ShellRuntimeConverterTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.params.ParameterizedTest;
10 | import org.junit.jupiter.params.provider.ValueSource;
11 |
12 | import static org.junit.jupiter.api.Assertions.assertNotNull;
13 |
14 | @DisplayName("ShellRuntimeConverter")
15 | class ShellRuntimeConverterTest {
16 |
17 | @ParameterizedTest
18 | @DisplayName("converts String to ShellRuntime")
19 | @ValueSource(strings = {
20 | "podman",
21 | "docker",
22 | "p",
23 | "d",
24 | "DOCKER",
25 | "PODMAN",
26 | "dOCkeR",
27 | "podMAN",
28 | "NERDCTL",
29 | "nerdctl",
30 | "nerdCTL",
31 | "n"
32 | })
33 | void shouldConvertStringToShellRuntime(final String input) {
34 | // given
35 | final var converter = new ShellRuntimeConverter();
36 |
37 | // when
38 | final var runtime = converter.convert(input);
39 |
40 | // then
41 | assertNotNull(runtime, input);
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/test/ClassTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.test;
7 |
8 | import java.lang.reflect.Modifier;
9 |
10 | import static java.lang.reflect.Modifier.isPublic;
11 | import static org.junit.jupiter.api.Assertions.assertNotNull;
12 | import static org.junit.jupiter.api.Assertions.assertTrue;
13 |
14 | public final class ClassTests {
15 |
16 | private ClassTests() {
17 | // helper class
18 | }
19 |
20 | public static void hasDefaultConstructor(final Class> clazz) throws NoSuchMethodException {
21 | final var constructor = clazz.getDeclaredConstructor();
22 | assertNotNull(constructor);
23 | assertTrue(constructor.trySetAccessible());
24 | assertTrue(constructor.canAccess(null));
25 | assertTrue(isPublic(constructor.getModifiers()));
26 | }
27 |
28 | public static void hasPrivateConstructor(final Class> clazz) throws NoSuchMethodException {
29 | final var constructor = clazz.getDeclaredConstructor();
30 | assertNotNull(constructor);
31 | assertTrue(Modifier.isPrivate(constructor.getModifiers()));
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/docs/themes/metio/layouts/_default/single.html:
--------------------------------------------------------------------------------
1 | {{ define "title" }}
2 | {{ .Site.Title }} – {{ .Title }}
3 | {{ end }}
4 | {{ define "main" }}
5 |
16 | {{ with .Params.tags }}
17 |
18 | Talks about:
19 | {{ $sort := sort . }}
20 | {{ $links := apply $sort "partial" "body/post-tag-link" "." }}
21 | {{ $clean := apply $links "chomp" "." }}
22 | {{ delimit $clean ", " ", and " }}
23 |
24 | {{ end }}
25 |
26 | {{ .Content }}
27 |
28 | {{ end }}
29 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/acceptance/HelpAcceptanceTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.acceptance;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.params.ParameterizedTest;
10 | import org.junit.jupiter.params.provider.ValueSource;
11 |
12 | import static org.junit.jupiter.api.Assertions.*;
13 |
14 | class HelpAcceptanceTest extends CLI_TCK {
15 |
16 | @DisplayName("usage help")
17 | @ParameterizedTest
18 | @ValueSource(strings = {"-h", "--help"})
19 | void shouldHaveUsageHelp(final String flag) {
20 | verifyHelp(flag);
21 | }
22 |
23 | @DisplayName("shell help")
24 | @ParameterizedTest
25 | @ValueSource(strings = {"-h", "--help"})
26 | void shouldHaveHelpForShell(final String flag) {
27 | verifyHelp("shell", flag);
28 | }
29 |
30 | private void verifyHelp(final String... flags) {
31 | final var exitCode = cmd.execute(flags);
32 | assertAll("help",
33 | () -> assertEquals(0, exitCode, "exitCode"),
34 | () -> assertTrue(output.toString().startsWith("Usage: ilo"), () -> output.toString()));
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/verify.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: The ilo Authors
2 | # SPDX-License-Identifier: 0BSD
3 |
4 | name: Verify Commits
5 | on:
6 | pull_request:
7 | branches: [ main ]
8 | jobs:
9 | verify:
10 | name: Build on ${{ matrix.os }}
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | matrix:
14 | os:
15 | - ubuntu-latest
16 | - macos-latest
17 | - windows-latest
18 | steps:
19 | - id: checkout
20 | name: Clone Git Repository
21 | uses: actions/checkout@v6
22 | - id: graal
23 | name: Set up GraalVM
24 | uses: graalvm/setup-graalvm@v1
25 | with:
26 | version: latest
27 | java-version: 21
28 | github-token: ${{ secrets.GITHUB_TOKEN }}
29 | - id: cache
30 | name: Cache Maven Repository
31 | uses: actions/cache@v5
32 | with:
33 | path: ~/.m2/repository
34 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
35 | restore-keys: |
36 | ${{ runner.os }}-maven-
37 | - id: verify
38 | name: Verify Project
39 | run: mvn --batch-mode --define skipNativeBuild=false verify
40 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: The ilo Authors
2 | # SPDX-License-Identifier: 0BSD
3 |
4 | name: CodeQL
5 | on:
6 | schedule:
7 | - cron: 37 13 * * 5
8 | jobs:
9 | codeql:
10 | name: Analyze
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | language: [ 'java' ]
16 | steps:
17 | - name: Clone Git Repository
18 | uses: actions/checkout@v6
19 | - id: graal
20 | name: Set up GraalVM
21 | uses: graalvm/setup-graalvm@v1
22 | with:
23 | version: latest
24 | java-version: 21
25 | github-token: ${{ secrets.GITHUB_TOKEN }}
26 | - id: cache
27 | name: Cache Maven Repository
28 | uses: actions/cache@v5
29 | with:
30 | path: ~/.m2/repository
31 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
32 | restore-keys: |
33 | ${{ runner.os }}-maven-
34 | - name: Initialize CodeQL
35 | uses: github/codeql-action/init@v4
36 | with:
37 | languages: ${{ matrix.language }}
38 | - name: Autobuild
39 | uses: github/codeql-action/autobuild@v4
40 | - name: Perform CodeQL Analysis
41 | uses: github/codeql-action/analyze@v4
42 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/IloTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.extension.ExtendWith;
11 | import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
12 | import uk.org.webcompere.systemstubs.properties.SystemProperties;
13 |
14 | import static org.junit.jupiter.api.Assertions.assertEquals;
15 | import static wtf.metio.ilo.test.TestResources.testResources;
16 |
17 | @DisplayName("Ilo")
18 | @ExtendWith(SystemStubsExtension.class)
19 | class IloTest {
20 |
21 | @Test
22 | @DisplayName("reads .rc files")
23 | void supportsRunCommands(final SystemProperties properties) {
24 | properties.set("user.dir", testResources(Ilo.class).resolve("root").toAbsolutePath().toString());
25 | assertEquals(1, Ilo.runCommands(new String[]{}).count());
26 | }
27 |
28 | @Test
29 | @DisplayName("does not need .rc files")
30 | void runsWithRunCommands(final SystemProperties properties) {
31 | properties.set("user.dir", testResources(Ilo.class).resolve("empty").toAbsolutePath().toString());
32 | assertEquals(0, Ilo.runCommands(new String[]{}).count());
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/os/ParameterExpansion.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.os;
7 |
8 | import java.util.function.Function;
9 | import java.util.regex.Pattern;
10 |
11 | abstract class ParameterExpansion {
12 |
13 | // visible for testing
14 | static final String MATCHER_GROUP_NAME = "expression";
15 |
16 | abstract String substituteCommands(String value);
17 |
18 | abstract String expandParameters(String value);
19 |
20 | // visible for testing
21 | final String replace(final String value, final Function super String, String> replacer, final Pattern... patterns) {
22 | var current = value;
23 | for (final var pattern : patterns) {
24 | current = replace(current, replacer, pattern);
25 | }
26 | return current;
27 | }
28 |
29 | private String replace(final String value, final Function super String, String> replacer, final Pattern pattern) {
30 | final var builder = new StringBuilder();
31 | final var matcher = pattern.matcher(value);
32 | while (matcher.find()) {
33 | matcher.appendReplacement(builder, replacer.apply(matcher.group(MATCHER_GROUP_NAME)));
34 | }
35 | matcher.appendTail(builder);
36 | return builder.toString();
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/docs/content/usage/autocomplete.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Autocomplete
3 | date: 2020-04-13
4 | menu:
5 | main:
6 | parent: usage
7 | identifier: usage_autocomplete
8 | categories:
9 | - usage
10 | tags:
11 | - autocomplete
12 | ---
13 |
14 | The `ilo generate-completion` command generates autocompletion configuration for shells such as [bash](https://www.gnu.org/software/bash/) and [zsh](https://www.zsh.org/).
15 |
16 | Once enabled you can use the `` key to autocomplete ilo commands and their options:
17 |
18 | ```console
19 | # autocomplete commands
20 | $ ilo s
21 | $ ilo shell
22 |
23 | # autocomplete options
24 | $ ilo shell --re
25 | $ ilo shell --remove-image
26 | ```
27 |
28 | ## bash
29 |
30 | In order to integrate autocompletion into [bash](https://www.gnu.org/software/bash/), follow these steps:
31 |
32 | 1. Create or edit `~/.bashrc`.
33 | 2. Add the following line
34 | ```shell
35 | source <(ilo generate-completion)
36 | ```
37 | 3. Reload your shell (or create a new one)
38 |
39 | ## zsh
40 |
41 | In order to integrate autocompletion into [zsh](https://www.zsh.org/), follow these steps:
42 |
43 | 1. Create or edit `$ZDOTDIR/.zshrc`.
44 | 2. Add the following line
45 | ```shell
46 | source <(ilo generate-completion)
47 | ```
48 | 3. Reload your shell (or create a new one)
49 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/errors/PrintingExceptionHandlerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.errors;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import wtf.metio.ilo.Ilo;
11 |
12 | import java.io.PrintWriter;
13 | import java.io.StringWriter;
14 |
15 | import static org.junit.jupiter.api.Assertions.assertAll;
16 | import static org.junit.jupiter.api.Assertions.assertEquals;
17 |
18 | @DisplayName("PrintingExceptionHandler")
19 | class PrintingExceptionHandlerTest {
20 |
21 | @Test
22 | @DisplayName("prints exception message to error output")
23 | void printsExceptionMessage() {
24 | final var commandLine = Ilo.commandLine();
25 | final var writer = new StringWriter();
26 | final var output = new PrintWriter(writer);
27 | commandLine.setErr(output);
28 | final var handler = new PrintingExceptionHandler();
29 |
30 | final var exception = new NoMatchingRuntimeException();
31 | final var exitCode = handler.handleExecutionException(exception, commandLine, null);
32 |
33 | assertAll("exceptions",
34 | () -> assertEquals(exception.getExitCode(), exitCode),
35 | () -> assertEquals("No matching runtime was found on your system. Select another runtime using '--runtime' or install your preferred runtime on your system." + System.lineSeparator(), writer.toString()));
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/website.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: The ilo Authors
2 | # SPDX-License-Identifier: 0BSD
3 |
4 | name: Publish Website
5 | on:
6 | schedule:
7 | - cron: 45 4 * * MON
8 | push:
9 | branches:
10 | - main
11 | paths:
12 | - docs/**
13 | jobs:
14 | website:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - id: checkout
18 | name: Clone Git Repository
19 | uses: actions/checkout@v6
20 | with:
21 | fetch-depth: 0
22 | - id: hugo
23 | name: Setup Hugo
24 | uses: peaceiris/actions-hugo@v3
25 | with:
26 | hugo-version: latest
27 | - id: previous
28 | name: Get Last Release
29 | run: echo "::set-output name=version::$(git describe --abbrev=0 --tags)"
30 | - id: build
31 | name: Build Website
32 | run: hugo --minify --printI18nWarnings --printPathWarnings --printUnusedTemplates --source docs
33 | env:
34 | ILO_RELEASE: ${{ steps.previous.outputs.version }}
35 | - id: htmltest
36 | name: Run htmltest
37 | uses: wjdp/htmltest-action@master
38 | with:
39 | config: ./docs/htmltest.yml
40 | - id: deploy
41 | name: Deploy Website
42 | uses: peaceiris/actions-gh-pages@v4
43 | with:
44 | github_token: ${{ secrets.GITHUB_TOKEN }}
45 | publish_dir: ./docs/public
46 | force_orphan: true
47 | cname: ilo.projects.metio.wtf
48 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/architecture/ArchitectureTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.architecture;
7 |
8 | import com.tngtech.archunit.core.domain.JavaClasses;
9 | import com.tngtech.archunit.core.importer.ClassFileImporter;
10 | import com.tngtech.archunit.core.importer.ImportOption;
11 | import org.junit.jupiter.api.*;
12 | import wtf.metio.ilo.Ilo;
13 | import wtf.metio.ilo.test.ArchUnitTests;
14 |
15 | import java.util.stream.Stream;
16 |
17 | @DisplayName("Architecture")
18 | public final class ArchitectureTest {
19 |
20 | private static JavaClasses classes;
21 |
22 | @BeforeAll
23 | static void importPackages() {
24 | classes = new ClassFileImporter()
25 | .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
26 | .importPackagesOf(Ilo.class);
27 | }
28 |
29 | @TestFactory
30 | @DisplayName("Global Rules")
31 | Stream globalRules() {
32 | return Stream.of(CodingRules.class, StructureRules.class, LayerRules.class)
33 | .map(clazz -> ArchUnitTests.in(clazz, rule -> rule.check(classes)));
34 | }
35 |
36 | @TestFactory
37 | @DisplayName("Implementation Rules")
38 | @Disabled
39 | Stream implementationRules() {
40 | return Stream.of();
41 | //return Stream.of(CliRules.class, ErrorsRules.class, ToolsRules.class)
42 | // .map(clazz -> ArchUnitTests.in(clazz, rule -> rule.check(classes)));
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/test/TestCliExecutor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.test;
7 |
8 | import wtf.metio.ilo.model.CliExecutor;
9 | import wtf.metio.ilo.model.CliTool;
10 | import wtf.metio.ilo.model.Options;
11 | import wtf.metio.ilo.model.Runtime;
12 |
13 | import java.util.*;
14 |
15 | public abstract class TestCliExecutor, CLI extends CliTool, OPTIONS extends Options>
16 | implements CliExecutor {
17 |
18 | private final List> collectedArguments = new ArrayList<>(4);
19 | private final ArrayDeque exitCodes = new ArrayDeque<>(4);
20 |
21 | @Override
22 | public final int execute(final List arguments, final boolean debug) {
23 | collectedArguments.add(arguments);
24 | return Optional.ofNullable(exitCodes.pollFirst()).orElse(0);
25 | }
26 |
27 | public final List pullArguments() {
28 | return collectedArguments.get(0);
29 | }
30 |
31 | public final List buildArguments() {
32 | return collectedArguments.get(1);
33 | }
34 |
35 | public final List runArguments() {
36 | return collectedArguments.get(2);
37 | }
38 |
39 | public final List cleanupArguments() {
40 | return collectedArguments.get(3);
41 | }
42 |
43 | public final void exitCodes(final Integer... codes) {
44 | exitCodes.addAll(Arrays.asList(codes));
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/shell/ShellCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import picocli.CommandLine;
9 | import wtf.metio.ilo.cli.CommandLifecycle;
10 | import wtf.metio.ilo.model.CliExecutor;
11 | import wtf.metio.ilo.version.VersionProvider;
12 |
13 | import java.util.concurrent.Callable;
14 |
15 | @CommandLine.Command(
16 | name = "shell",
17 | description = "Opens an (interactive) shell for your build environment",
18 | versionProvider = VersionProvider.class,
19 | mixinStandardHelpOptions = true,
20 | showAtFileInUsageHelp = true,
21 | usageHelpAutoWidth = true,
22 | showDefaultValues = true,
23 | descriptionHeading = "%n",
24 | parameterListHeading = "%n"
25 | )
26 | public final class ShellCommand implements Callable {
27 |
28 | @CommandLine.Mixin
29 | public ShellOptions options;
30 |
31 | private final CliExecutor super ShellRuntime, ShellCLI, ShellOptions> executor;
32 |
33 | // default constructor for picocli
34 | public ShellCommand() {
35 | this(new ShellExecutor());
36 | }
37 |
38 | // constructor for testing
39 | ShellCommand(final CliExecutor super ShellRuntime, ShellCLI, ShellOptions> executor) {
40 | this.executor = executor;
41 | }
42 |
43 | @Override
44 | public Integer call() {
45 | final var tool = executor.selectRuntime(options.runtime);
46 | return CommandLifecycle.run(tool, options, executor::execute);
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/model/CliTool.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.model;
7 |
8 | import wtf.metio.ilo.cli.Executables;
9 |
10 | import java.util.List;
11 |
12 | /**
13 | * CLI tools are used by 'ilo' in order to do most of its work. This interface represents such a tool.
14 | */
15 | public interface CliTool {
16 |
17 | /**
18 | * @return The name of the CLI tool.
19 | */
20 | String name();
21 |
22 | /**
23 | * @return The CLI subcommand to use.
24 | */
25 | default String command() {
26 | return "";
27 | }
28 |
29 | /**
30 | * @return Whether this CLI tool is installed and executable.
31 | */
32 | default boolean exists() {
33 | return Executables.of(name()).isPresent();
34 | }
35 |
36 | /**
37 | * @param options The options to use.
38 | * @return The command line for the 'pull' step.
39 | */
40 | List pullArguments(OPTIONS options);
41 |
42 | /**
43 | * @param options The options to use.
44 | * @return The command line for the 'build' step.
45 | */
46 | List buildArguments(OPTIONS options);
47 |
48 | /**
49 | * @param options The options to use.
50 | * @return The command line for the 'run' step.
51 | */
52 | List runArguments(OPTIONS options);
53 |
54 | /**
55 | * @param options The options to use.
56 | * @return The command line for the 'cleanup' step.
57 | */
58 | List cleanupArguments(OPTIONS options);
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/docs/content/shell/runtimes.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Runtimes
3 | date: 2020-04-13
4 | menu:
5 | main:
6 | parent: shell
7 | identifier: shell_runtimes
8 | categories:
9 | - shell
10 | tags:
11 | - runtime
12 | - docker
13 | - podman
14 | - nerdctl
15 | ---
16 |
17 | `ilo shell` by default searches your local system for supported runtimes. In order to force the usage of a specific runtime, use the `--runtime` flag or set the `ILO_SHELL_RUNTIME` environment variable in your system. The `--runtime` flag overwrites the environment variable.
18 |
19 | ## Docker
20 |
21 | Force `ilo` to use [docker](https://www.docker.com/) like this:
22 |
23 | ```console
24 | $ ilo shell --runtime docker
25 |
26 | # use alias
27 | $ ilo shell --runtime d
28 |
29 | # use env variable
30 | $ ILO_SHELL_RUNTIME=docker ilo shell
31 | ```
32 |
33 | ## nerdctl
34 |
35 | Force `ilo` to use [nerdctl](https://github.com/containerd/nerdctl) like this:
36 |
37 | ```console
38 | $ ilo shell --runtime nerdctl
39 |
40 | # use alias
41 | $ ilo shell --runtime n
42 |
43 | # use env variable
44 | $ ILO_SHELL_RUNTIME=nerdctl ilo shell
45 | ```
46 |
47 | ## Podman
48 |
49 | Force `ilo` to use [podman](https://podman.io/) like this:
50 |
51 | ```console
52 | $ ilo shell --runtime podman
53 |
54 | # use alias
55 | $ ilo shell --runtime p
56 |
57 | # use env variable
58 | $ ILO_SHELL_RUNTIME=podman ilo shell
59 | ```
60 |
61 | ## Auto Selection
62 |
63 | If not otherwise specified, `ilo` always picks runtimes in this order, depending on which are available on your system:
64 |
65 | 1. podman
66 | 2. nerdctl
67 | 3. docker
68 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/model/CliExecutorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.model;
7 |
8 | import org.junit.jupiter.api.Assertions;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.condition.EnabledOnOs;
11 | import org.junit.jupiter.api.condition.OS;
12 | import wtf.metio.ilo.errors.RuntimeIOException;
13 |
14 | import java.util.List;
15 |
16 | class CliExecutorTest {
17 |
18 | @Test
19 | void shouldExecuteMissingCommands() {
20 | final var executor = new CliExecutor<>() {
21 |
22 | @Override
23 | public CliTool> selectRuntime(final Runtime runtime) {
24 | return null;
25 | }
26 |
27 | };
28 |
29 | Assertions.assertThrows(RuntimeIOException.class,
30 | () -> executor.execute(List.of("some", "command"), false));
31 | }
32 |
33 | @Test
34 | @EnabledOnOs({OS.LINUX, OS.MAC})
35 | void shouldExecuteCommandOnUnix() {
36 | final var executor = new CliExecutor<>() {
37 |
38 | @Override
39 | public CliTool> selectRuntime(final Runtime runtime) {
40 | return null;
41 | }
42 |
43 | };
44 |
45 | Assertions.assertEquals(0, executor.execute(List.of("ls"), false));
46 | }
47 |
48 | @Test
49 | @EnabledOnOs({OS.WINDOWS})
50 | void shouldExecuteCommandOnWindows() {
51 | final var executor = new CliExecutor<>() {
52 |
53 | @Override
54 | public CliTool> selectRuntime(final Runtime runtime) {
55 | return null;
56 | }
57 |
58 | };
59 |
60 | Assertions.assertEquals(0, executor.execute(List.of("dir"), false));
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/docs/themes/metio/layouts/partials/body/sidebar.html:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/utils/Streams.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.utils;
7 |
8 | import java.util.Arrays;
9 | import java.util.Collection;
10 | import java.util.List;
11 | import java.util.Objects;
12 | import java.util.stream.Stream;
13 |
14 | import static java.util.function.Function.identity;
15 | import static java.util.function.Predicate.not;
16 | import static java.util.stream.Stream.of;
17 |
18 | public final class Streams {
19 |
20 | public static Stream filter(final Stream stream) {
21 | return stream
22 | .filter(Objects::nonNull)
23 | .filter(not(String::isBlank));
24 | }
25 |
26 | public static Stream fromList(final List list) {
27 | return Stream.ofNullable(list).flatMap(Collection::stream);
28 | }
29 |
30 | @SafeVarargs
31 | public static List flatten(final Stream... streams) {
32 | return filter(Arrays.stream(streams).flatMap(identity())).toList();
33 | }
34 |
35 | public static Stream maybe(final boolean condition, final String... values) {
36 | return condition ? filter(Arrays.stream(values)) : Stream.empty();
37 | }
38 |
39 | public static Stream optional(final String prefix, final String value) {
40 | return Objects.nonNull(value) && !value.isBlank() ? of(prefix, value) : Stream.empty();
41 | }
42 |
43 | public static Stream withPrefix(final String prefix, final List values) {
44 | return filter(fromList(values)).flatMap(value -> of(prefix, value));
45 | }
46 |
47 | private Streams() {
48 | // utility class
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/test/CliToolTCK.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.test;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import wtf.metio.ilo.model.CliTool;
11 | import wtf.metio.ilo.model.Options;
12 |
13 | import static org.junit.jupiter.api.Assertions.assertEquals;
14 | import static org.junit.jupiter.api.Assertions.assertNotNull;
15 |
16 | public abstract class CliToolTCK> {
17 |
18 | protected abstract SHELL tool();
19 |
20 | protected abstract OPTIONS options();
21 |
22 | protected abstract String name();
23 |
24 | protected String command() {
25 | return "";
26 | }
27 |
28 | @Test
29 | @DisplayName("has runtime name")
30 | void shouldHaveName() {
31 | assertEquals(name(), tool().name());
32 | }
33 |
34 | @Test
35 | @DisplayName("has subcommand")
36 | void shouldHaveCommand() {
37 | assertEquals(command(), tool().command());
38 | }
39 |
40 | @Test
41 | @DisplayName("non-null pull arguments")
42 | void pullArguments() {
43 | assertNotNull(tool().pullArguments(options()));
44 | }
45 |
46 | @Test
47 | @DisplayName("non-null build arguments")
48 | void buildArguments() {
49 | assertNotNull(tool().buildArguments(options()));
50 | }
51 |
52 | @Test
53 | @DisplayName("non-null run arguments")
54 | void runArguments() {
55 | assertNotNull(tool().runArguments(options()));
56 | }
57 |
58 | @Test
59 | @DisplayName("non-null cleanup arguments")
60 | void cleanupArguments() {
61 | assertNotNull(tool().cleanupArguments(options()));
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/utils/StringsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.utils;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.params.ParameterizedTest;
11 | import org.junit.jupiter.params.provider.ValueSource;
12 | import wtf.metio.ilo.test.ClassTests;
13 |
14 | import static org.junit.jupiter.api.Assertions.assertFalse;
15 | import static org.junit.jupiter.api.Assertions.assertTrue;
16 |
17 | @DisplayName("Strings")
18 | class StringsTest {
19 |
20 | @DisplayName("isBlank")
21 | @ParameterizedTest
22 | @ValueSource(strings = {"", " ", " "})
23 | void shouldDetectBlankString(final String value) {
24 | assertTrue(Strings.isBlank(value));
25 | }
26 |
27 | @DisplayName("isBlank")
28 | @ParameterizedTest
29 | @ValueSource(strings = {"a", " b ", " c "})
30 | void shouldDetectBlankStringWithValues(final String value) {
31 | assertFalse(Strings.isBlank(value));
32 | }
33 |
34 | @DisplayName("isBlank")
35 | @ParameterizedTest
36 | @ValueSource(strings = {"a", " b ", " c "})
37 | void shouldDetectNonBlankString(final String value) {
38 | assertTrue(Strings.isNotBlank(value));
39 | }
40 |
41 | @DisplayName("isBlank")
42 | @ParameterizedTest
43 | @ValueSource(strings = {"", " ", " "})
44 | void shouldDetectNonBlankStringWithoutValues(final String value) {
45 | assertFalse(Strings.isNotBlank(value));
46 | }
47 |
48 | @Test
49 | @DisplayName("has private constructor")
50 | void shouldHavePrivateConstructor() throws NoSuchMethodException {
51 | ClassTests.hasPrivateConstructor(Strings.class);
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/shell/ShellExecutorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.DisplayName;
10 | import org.junit.jupiter.api.Test;
11 |
12 | import static org.junit.jupiter.api.Assertions.assertNotNull;
13 | import static org.junit.jupiter.api.Assumptions.assumeTrue;
14 |
15 | @DisplayName("ShellExecutor")
16 | class ShellExecutorTest {
17 |
18 | private ShellExecutor shellExecutor;
19 |
20 | @BeforeEach
21 | void setUp() {
22 | shellExecutor = new ShellExecutor();
23 | }
24 |
25 | @Test
26 | @DisplayName("returns non-null values for auto-selection")
27 | void shouldReturnNonNullValueForAutoSelection() {
28 | assumeTrue(new Podman().exists() || new Docker().exists() || new Nerdctl().exists());
29 | assertNotNull(shellExecutor.selectRuntime(null));
30 | }
31 |
32 | @Test
33 | @DisplayName("returns non-null values for forced podman usage")
34 | void shouldReturnNonNullValueForPodman() {
35 | assumeTrue(new Podman().exists());
36 | assertNotNull(shellExecutor.selectRuntime(ShellRuntime.PODMAN));
37 | }
38 |
39 | @Test
40 | @DisplayName("returns non-null values for forced docker usage")
41 | void shouldReturnNonNullValueForDocker() {
42 | assumeTrue(new Docker().exists());
43 | assertNotNull(shellExecutor.selectRuntime(ShellRuntime.DOCKER));
44 | }
45 |
46 | @Test
47 | @DisplayName("returns non-null values for forced nerdctl usage")
48 | void shouldReturnNonNullValueForNerdctl() {
49 | assumeTrue(new Nerdctl().exists());
50 | assertNotNull(shellExecutor.selectRuntime(ShellRuntime.NERDCTL));
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/os/OSSupport.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.os;
7 |
8 | import wtf.metio.ilo.cli.Executables;
9 |
10 | import java.nio.file.Path;
11 | import java.util.List;
12 | import java.util.Optional;
13 |
14 | import static java.util.stream.Collectors.toList;
15 | import static wtf.metio.ilo.utils.Streams.filter;
16 | import static wtf.metio.ilo.utils.Streams.fromList;
17 |
18 | public final class OSSupport {
19 |
20 | public static List expand(final List values) {
21 | return filter(fromList(values))
22 | .map(OSSupport::expand)
23 | .collect(toList());
24 | }
25 |
26 | public static String expand(final String value) {
27 | final var expansion = expansion();
28 | return Optional.ofNullable(value)
29 | .map(expansion::expandParameters)
30 | .map(expansion::substituteCommands)
31 | .orElse(value);
32 | }
33 |
34 | static ParameterExpansion expansion() {
35 | return posixShell()
36 | .or(OSSupport::powerShell)
37 | .orElseGet(NoOpExpansion::new);
38 | }
39 |
40 | // visible for testing
41 | static Optional posixShell() {
42 | return Executables.of("bash")
43 | .or(() -> Executables.of("zsh"))
44 | .or(() -> Executables.of("sh"))
45 | .map(Path::toAbsolutePath)
46 | .map(PosixShell::new);
47 | }
48 |
49 | static Optional powerShell() {
50 | return Executables.of("pwsh.exe")
51 | .or(() -> Executables.of("powershell.exe"))
52 | .or(() -> Executables.of("pwsh"))
53 | .map(Path::toAbsolutePath)
54 | .map(PowerShell::new);
55 | }
56 |
57 | private OSSupport() {
58 | // utility class
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/os/PowerShell.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.os;
7 |
8 | import wtf.metio.ilo.cli.Executables;
9 |
10 | import java.nio.file.Path;
11 | import java.util.regex.Pattern;
12 |
13 | /**
14 | * Support for Windows PowerShell
15 | *
16 | * @see PowerShell
17 | */
18 | final class PowerShell extends ParameterExpansion {
19 |
20 | private static final String COMMAND_STYLE = String.format("\\$\\((?<%s>[^)]+)\\)", MATCHER_GROUP_NAME);
21 | private static final String PARAMETER_STYLE = String.format("(?<%s>\\$[a-zA-Z][a-zA-Z0-9_]*)", MATCHER_GROUP_NAME);
22 |
23 | // visible for testing
24 | static final Pattern COMMAND_PATTERN = Pattern.compile(COMMAND_STYLE);
25 | static final Pattern PARAMETER_PATTERN = Pattern.compile(PARAMETER_STYLE);
26 |
27 | private final Path shellBinary;
28 |
29 | PowerShell(final Path shellBinary) {
30 | this.shellBinary = shellBinary;
31 | }
32 |
33 | @Override
34 | public String substituteCommands(final String value) {
35 | return replace(value,
36 | command -> Executables.runAndReadOutput(shellBinary.toString(), "-OutputFormat", "Text", "-Command", command),
37 | COMMAND_PATTERN);
38 | }
39 |
40 | @Override
41 | public String expandParameters(final String value) {
42 | return replace(expandTilde(value),
43 | parameter -> Executables.runAndReadOutput(shellBinary.toString(), "-OutputFormat", "Text", "-Command", "'Write-Output \"" + parameter + "\"'"),
44 | PARAMETER_PATTERN);
45 | }
46 |
47 | private String expandTilde(final String value) {
48 | final var userHome = System.getProperty("user.home");
49 | return value.replace("~", userHome);
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/docs/content/usage/build-envs.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Build Environments
3 | date: 2020-04-13
4 | menu:
5 | main:
6 | parent: usage
7 | identifier: usage_build_envs
8 | weight: 101
9 | categories:
10 | - usage
11 | tags:
12 | - build
13 | - env
14 | ---
15 |
16 | `ilo` allows you to define your build environment either in a [Containerfile/Dockerfile](https://docs.docker.com/engine/reference/builder/) or any other [OCI Image](https://github.com/opencontainers/image-spec/blob/master/spec.md) compliant way. In contrast to [toolbx](https://containertoolbx.org/), `ilo` relies on immutable containers which makes it easier to share those images across your team. `ilo` uses the same mechanism to define build environments that developers are already using to define their application run environments. Therefore, onboarding and adapting container based build environments should be easy for most teams.
17 |
18 | As an example, consider the following Containerfile that is based on the official [Maven image](https://hub.docker.com/_/maven) and extends that with another binary ([hugo](https://gohugo.io/) in this case).
19 |
20 | ```console
21 | # write some Containerfile
22 | $ cat your.containerfile
23 | FROM maven:3-openjdk-11-slim
24 |
25 | RUN apt-get update && apt-get install hugo -y
26 | ```
27 |
28 | This image can be build just like any other image with your typical tooling, e.g. using [podman](https://podman.io/):
29 |
30 | ```console
31 | $ podman build --tag your.image:your.tag --file your.containerfile path/to/build/context
32 | ```
33 |
34 | The idea behind `ilo` is that you use this image to start a container that mounts your project directory and is able to execute any command that you are using to build/test/package your project.
35 |
36 | Take a look at the detailed instructions for [ilo shell](../../shell) on how to use your created image.
37 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/utils/StreamsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.utils;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import wtf.metio.ilo.test.ClassTests;
11 |
12 | import java.util.List;
13 | import java.util.stream.Stream;
14 |
15 | import static org.junit.jupiter.api.Assertions.assertEquals;
16 |
17 | @DisplayName("Streams")
18 | class StreamsTest {
19 |
20 | @Test
21 | void filterNonStrings() {
22 | assertEquals(1, Streams.filter(Stream.of("first", "", null)).count());
23 | }
24 |
25 | @Test
26 | void listToStream() {
27 | assertEquals(2, Streams.fromList(List.of("first", "")).count());
28 | }
29 |
30 | @Test
31 | void nullListToStream() {
32 | assertEquals(0, Streams.fromList(null).count());
33 | }
34 |
35 | @Test
36 | void flattenStreams() {
37 | assertEquals(2, Streams.flatten(Stream.of("first"), Stream.of("second")).size());
38 | }
39 |
40 | @Test
41 | void maybe() {
42 | assertEquals(2, Streams.maybe(true, "first", "second").count());
43 | }
44 |
45 | @Test
46 | void maybeNot() {
47 | assertEquals(0, Streams.maybe(false, "first", "second").count());
48 | }
49 |
50 | @Test
51 | void optional() {
52 | assertEquals(2, Streams.optional("--option", "value").count());
53 | }
54 |
55 | @Test
56 | void optionalNot() {
57 | assertEquals(0, Streams.optional("--option", null).count());
58 | }
59 |
60 | @Test
61 | void optionalEmpty() {
62 | assertEquals(0, Streams.optional("--option", "").count());
63 | }
64 |
65 | @Test
66 | @DisplayName("has private constructor")
67 | void shouldHavePrivateConstructor() throws NoSuchMethodException {
68 | ClassTests.hasPrivateConstructor(Streams.class);
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/test/ArchUnitTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.test;
7 |
8 | import com.tngtech.archunit.junit.ArchTest;
9 | import com.tngtech.archunit.lang.ArchRule;
10 | import org.junit.jupiter.api.DisplayName;
11 | import org.junit.jupiter.api.DynamicNode;
12 |
13 | import java.lang.reflect.Field;
14 | import java.util.Arrays;
15 | import java.util.function.Consumer;
16 | import java.util.stream.Stream;
17 |
18 | import static java.lang.reflect.Modifier.*;
19 | import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
20 | import static org.junit.jupiter.api.DynamicTest.dynamicTest;
21 |
22 | public final class ArchUnitTests {
23 |
24 | private ArchUnitTests() {
25 | // helper class
26 | }
27 |
28 | public static DynamicNode in(final Class> clazz, final Consumer super ArchRule> check) {
29 | final var displayName = clazz.getAnnotation(DisplayName.class);
30 | return dynamicContainer(displayName.value(), in(clazz)
31 | .map(rule -> dynamicTest(rule.getDescription(), () -> check.accept(rule))));
32 | }
33 |
34 | public static Stream in(final Class> clazz) {
35 | return Arrays.stream(clazz.getDeclaredFields())
36 | .filter(field -> field.isAnnotationPresent(ArchTest.class))
37 | .filter(field -> ArchRule.class.isAssignableFrom(field.getType()))
38 | .filter(field -> isPublic(field.getModifiers()))
39 | .filter(field -> isStatic(field.getModifiers()))
40 | .filter(field -> isFinal(field.getModifiers()))
41 | .map(ArchUnitTests::value);
42 | }
43 |
44 | static ArchRule value(final Field field) {
45 | try {
46 | return (ArchRule) field.get(null);
47 | } catch (final IllegalAccessException exception) {
48 | throw new RuntimeException(exception);
49 | }
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/docs/themes/metio/layouts/partials/body/site-header.html:
--------------------------------------------------------------------------------
1 | {{ block "header" . }}
2 |
36 | {{ end }}
37 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/shell/ShellRuntime.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import wtf.metio.ilo.cli.EnvironmentVariables;
9 | import wtf.metio.ilo.errors.NoMatchingRuntimeException;
10 | import wtf.metio.ilo.model.CliTool;
11 | import wtf.metio.ilo.model.Runtime;
12 |
13 | import java.util.Arrays;
14 | import java.util.Optional;
15 |
16 | public enum ShellRuntime implements Runtime {
17 |
18 | PODMAN(new Podman(), "podman", "p"),
19 | NERDCTL(new Nerdctl(), "nerdctl", "n"),
20 | DOCKER(new Docker(), "docker", "d");
21 |
22 | private final ShellCLI cli;
23 | private final String[] aliases;
24 |
25 | ShellRuntime(final ShellCLI cli, final String... aliases) {
26 | this.cli = cli;
27 | this.aliases = aliases;
28 | }
29 |
30 | public static ShellRuntime fromAlias(final String alias) {
31 | return Runtime.firstMatching(alias, values());
32 | }
33 |
34 | @Override
35 | public String[] aliases() {
36 | return aliases;
37 | }
38 |
39 | @Override
40 | public ShellCLI cli() {
41 | return cli;
42 | }
43 |
44 | /**
45 | * Select a runtime for 'ilo shell'.
46 | *
47 | * @param preferred The runtime to force, or null for auto-selection.
48 | * @return The selected shell runtime.
49 | */
50 | public static ShellCLI autoSelect(final ShellRuntime preferred) {
51 | return Optional.ofNullable(preferred)
52 | .or(() -> Optional.ofNullable(System.getenv(EnvironmentVariables.ILO_SHELL_RUNTIME.name()))
53 | .map(ShellRuntime::fromAlias))
54 | .map(ShellRuntime::cli)
55 | .or(() -> Arrays.stream(ShellRuntime.values())
56 | .map(ShellRuntime::cli)
57 | .filter(CliTool::exists)
58 | .findFirst())
59 | .filter(CliTool::exists)
60 | .orElseThrow(NoMatchingRuntimeException::new);
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/.github/workflows/update-parent.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: The ilo Authors
2 | # SPDX-License-Identifier: 0BSD
3 |
4 | name: Update Parent
5 | on:
6 | schedule:
7 | - cron: 0 1 2 * *
8 | jobs:
9 | parent:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - id: checkout
13 | name: Clone Git Repository
14 | uses: actions/checkout@v6
15 | - id: graal
16 | name: Set up GraalVM
17 | uses: graalvm/setup-graalvm@v1
18 | with:
19 | version: latest
20 | java-version: 21
21 | github-token: ${{ secrets.GITHUB_TOKEN }}
22 | - id: cache
23 | uses: actions/cache@v5
24 | with:
25 | path: ~/.m2/repository
26 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
27 | restore-keys: |
28 | ${{ runner.os }}-maven-
29 | - id: parent
30 | name: Update parent
31 | run: mvn --batch-mode --define generateBackupPoms=false versions:update-parent
32 | - id: cpr
33 | name: Create Pull Request
34 | uses: peter-evans/create-pull-request@v8
35 | with:
36 | token: ${{ secrets.PAT }}
37 | commit-message: update parent to latest version
38 | committer: GitHub
39 | author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
40 | title: update parent to latest version
41 | body: |
42 | Project updated with: `mvn versions:update-parent`
43 | labels: dependencies
44 | assignees: sebhoss
45 | draft: false
46 | base: main
47 | branch: update-parent
48 | delete-branch: true
49 | - name: Enable Pull Request Automerge
50 | if: steps.cpr.outputs.pull-request-operation == 'created'
51 | run: gh pr merge --auto --rebase "${{ steps.cpr.outputs.pull-request-number }}"
52 | env:
53 | GITHUB_TOKEN: ${{ secrets.PAT }}
54 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/shell/ShellOptionsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.params.ParameterizedTest;
11 | import org.junit.jupiter.params.provider.ValueSource;
12 | import wtf.metio.ilo.test.ClassTests;
13 |
14 | import java.lang.reflect.Modifier;
15 |
16 | import static org.junit.jupiter.api.Assertions.assertEquals;
17 | import static org.junit.jupiter.api.Assertions.assertTrue;
18 |
19 | @DisplayName("ShellOptions")
20 | class ShellOptionsTest {
21 |
22 | @Test
23 | @DisplayName("has default constructor")
24 | void shouldHaveDefaultConstructor() throws NoSuchMethodException {
25 | ClassTests.hasDefaultConstructor(ShellOptions.class);
26 | }
27 |
28 | @ParameterizedTest
29 | @DisplayName("has public fields")
30 | @ValueSource(strings = {
31 | "runtime",
32 | "debug",
33 | "interactive",
34 | "pull",
35 | "containerfile",
36 | "hostname",
37 | "removeImage",
38 | "runtimeOptions",
39 | "runtimePullOptions",
40 | "runtimeBuildOptions",
41 | "runtimeRunOptions",
42 | "runtimeCleanupOptions",
43 | "volumes",
44 | "variables",
45 | "ports",
46 | "image",
47 | "mountProjectDir",
48 | "commands"
49 | })
50 | void shouldHavePublicProperty(final String field) throws NoSuchFieldException {
51 | final var runtime = ShellOptions.class.getDeclaredField(field);
52 | assertTrue(Modifier.isPublic(runtime.getModifiers()));
53 | }
54 |
55 | @ParameterizedTest
56 | @DisplayName("returns debug value")
57 | @ValueSource(booleans = {true, false})
58 | void shouldReturnDebugValue(final boolean value) {
59 | final var options = new ShellOptions();
60 | options.debug = value;
61 | assertEquals(value, options.debug());
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: The ilo Authors
2 | # SPDX-License-Identifier: 0BSD
3 |
4 | MAKEFLAGS += --warn-undefined-variables
5 | SHELL = /bin/bash
6 | .SHELLFLAGS := -eu -o pipefail -c
7 | .DEFAULT_GOAL := all
8 | .DELETE_ON_ERROR:
9 | .SUFFIXES:
10 |
11 | TIMESTAMPED_VERSION := $(shell /bin/date "+%Y.%m.%d-%H%M%S")
12 | CURRENT_DATE := $(shell /bin/date "+%Y-%m-%d")
13 | USERNAME := $(shell id -u -n)
14 | USERID := $(shell id -u)
15 | GREEN := $(shell tput -Txterm setaf 2)
16 | WHITE := $(shell tput -Txterm setaf 7)
17 | YELLOW := $(shell tput -Txterm setaf 3)
18 | RESET := $(shell tput -Txterm sgr0)
19 |
20 | HELP_FUN = \
21 | %help; \
22 | while(<>) { push @{$$help{$$2 // 'targets'}}, [$$1, $$3] if /^([a-zA-Z0-9\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \
23 | print "usage: make [target]\n\n"; \
24 | for (sort keys %help) { \
25 | print "${WHITE}$$_:${RESET}\n"; \
26 | for (@{$$help{$$_}}) { \
27 | $$sep = " " x (32 - length $$_->[0]); \
28 | print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \
29 | }; \
30 | print "\n"; }
31 |
32 | .PHONY: all
33 | all: help
34 |
35 | .PHONY: help
36 | help: ##@other Show this help
37 | @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)
38 |
39 | .PHONY: build
40 | build: ##@hacking Build everything
41 | mvn verify
42 |
43 | .PHONY: native-image
44 | native-image: ##@hacking Create a native image using GraalVM
45 | mvn verify --define skipNativeBuild=false
46 |
47 | .PHONY: clean
48 | clean: ##@hacking Clean build artifacts
49 | mvn clean
50 |
51 | .PHONY: ilo-build
52 | ilo-build: ##@hacking Build everything with ilo
53 | ilo @dev/build
54 |
55 | .PHONY: ilo-native
56 | ilo-native: ##@hacking Create a native image using GraalVM with ilo
57 | ilo @dev/native
58 |
59 | .PHONY: ilo-env
60 | ilo-env: ##@hacking Open a new development environment with ilo
61 | ilo @dev/env
62 |
63 | .PHONY: ilo-website
64 | ilo-website: ##@hacking Build the website with ilo
65 | ilo @dev/website
66 |
67 | .PHONY: ilo-serve
68 | ilo-serve: ##@hacking Serve the website locally with ilo
69 | ilo @dev/serve
70 |
--------------------------------------------------------------------------------
/docs/themes/metio/layouts/partials/head/styles.html:
--------------------------------------------------------------------------------
1 | {{ $normalize := resources.Get "/css/normalize.css" }}
2 | {{ $font := resources.Get "/css/font.css" }}
3 | {{ $header := resources.Get "/css/header.css" }}
4 | {{ $footer := resources.Get "/css/footer.css" }}
5 | {{ $navigation := resources.Get "/css/navigation.css" }}
6 | {{ $navigation_mobile := resources.Get "/css/navigation-mobile.css" }}
7 | {{ $navigation_tablet := resources.Get "/css/navigation-tablet.css" }}
8 | {{ $navigation_desktop := resources.Get "/css/navigation-desktop.css" }}
9 | {{ $layout := resources.Get "/css/layout.css" }}
10 | {{ $layout_mobile := resources.Get "/css/layout-mobile.css" }}
11 | {{ $layout_tablet := resources.Get "/css/layout-tablet.css" }}
12 | {{ $layout_desktop := resources.Get "/css/layout-desktop.css" }}
13 | {{ $syntax := resources.Get "/css/syntax.css" }}
14 | {{ $darkmode := resources.Get "/css/darkmode.css" | minify | fingerprint "sha512" }}
15 |
16 | {{ $mobile := slice $normalize $font $header $footer $navigation $layout $syntax $navigation_mobile $layout_mobile | resources.Concat "css/mobile.css" | minify | fingerprint "sha512" }}
17 | {{ $tablet := slice $normalize $font $header $footer $navigation $layout $syntax $navigation_tablet $layout_tablet | resources.Concat "css/tablet.css" | minify | fingerprint "sha512" }}
18 | {{ $desktop := slice $normalize $font $header $footer $navigation $layout $syntax $navigation_desktop $layout_desktop | resources.Concat "css/desktop.css" | minify | fingerprint "sha512" }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/content/contributors/building.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Building
3 | date: 2020-04-13
4 | menu:
5 | main:
6 | parent: 'Contributors'
7 | categories:
8 | - Contributors
9 | tags:
10 | - build
11 | - environment
12 | ---
13 |
14 | `ilo` requires a certain set of software installed on your system in order to be built.
15 |
16 | ## Prerequisites
17 |
18 | - [git](https://git-scm.com/) to fetch the [source code](../git-mirrors) of `ilo`
19 |
20 | ## ilo Setup
21 |
22 | You can use `ilo` to build `ilo`! Make sure your system has the following:
23 |
24 | - [ilo](../../usage/install) to open the reproducible build environment for `ilo` itself
25 | - One of the [runtimes](../../shell/runtimes) that `ilo shell` supports.
26 |
27 | ## Manual Setup
28 |
29 | In case you do not have `ilo` installed on your system, install the following manually:
30 |
31 | - [Java JDK](https://jdk.java.net/) to compile the code
32 | - [Maven](https://maven.apache.org/) to build the project
33 | - [hugo](https://gohugo.io/) in order to create the website
34 | - [GraalVM](https://www.graalvm.org/) to build a native executable
35 |
36 | ## Building
37 |
38 | ### Using ilo
39 |
40 | In case you have `ilo` installed, call this:
41 |
42 | ```console
43 | # open a shell with a pre-defined build environment
44 | $ ilo @dev/env
45 |
46 | # build the project
47 | $ ilo @dev/build
48 |
49 | # build native executable
50 | $ ilo @dev/native
51 |
52 | # build website
53 | $ ilo @dev/website
54 |
55 | # serve website
56 | $ ilo @dev/serve
57 | ```
58 |
59 | ### Without ilo
60 |
61 | In order to build `ilo` without having `ilo` installed call:
62 |
63 | ```console
64 | # build the project
65 | $ mvn verify
66 |
67 | # build native executable
68 | $ mvn verify --define skipNativeBuild=false
69 | ```
70 |
71 | In case you want to build or work on the website do this:
72 |
73 | ```console
74 | # build website
75 | $ hugo --minify --printI18nWarnings --printPathWarnings --printUnusedTemplates --source docs
76 |
77 | # serve website
78 | $ hugo server --minify --printI18nWarnings --printPathWarnings --printUnusedTemplates --source docs --watch
79 | ```
80 |
--------------------------------------------------------------------------------
/docs/content/shell/examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples
3 | date: 2020-04-13
4 | menu:
5 | main:
6 | parent: shell
7 | identifier: shell_examples
8 | categories:
9 | - shell
10 | tags:
11 | - examples
12 | ---
13 |
14 | The following examples show how `ilo shell` can be used.
15 |
16 | ## Cargo Projects
17 |
18 | [Cargo](https://doc.rust-lang.org/cargo/) caches all downloaded dependencies in your local `~/.cargo/registry` directory.
19 |
20 | In order to re-use already downloaded dependencies inside the container, specify a `--volumne` like this:
21 |
22 | ```console
23 | # Cargo project that mounts local .cargo folder
24 | $ ilo shell \
25 | --volume ${HOME}/.cargo/registry:/usr/local/cargo/registry:z \
26 | rust:latest
27 | ```
28 |
29 | **Note**: The container path `/usr/local/cargo` is specified in the image used in this example (`rust:latest`). Adjust this value according to the image you are actually using in your project.
30 |
31 | ## Gradle Projects
32 |
33 | [Gradle](https://gradle.org/) caches all downloaded dependencies in your local `~/.gradle` directory.
34 |
35 | In order to re-use already downloaded dependencies inside the container, specify a `--volumne` like this:
36 |
37 | ```console
38 | # Gradle project that mounts local .gradle folder
39 | $ ilo shell \
40 | --volume ${HOME}/.gradle:/home/gradle/.gradle:z \
41 | gradle:latest
42 | ```
43 |
44 | **Note**: The container path `/home/gradle/.gradle` is specified in the image used in this example (`gradle:latest`). Adjust this value according to the image you are actually using in your project.
45 |
46 | ## Maven Projects
47 |
48 | [Maven](https://maven.apache.org/) caches all downloaded dependencies in your local `~/.m2` directory.
49 |
50 | In order to re-use already downloaded dependencies inside the container, specify a `--volumne` like this:
51 |
52 | ```console
53 | # Maven project that mounts local m2 repo
54 | $ ilo shell \
55 | --volume ${HOME}/.m2:/root/.m2:z \
56 | maven:latest
57 | ```
58 |
59 | **Note**: The container path `/root/.m2` is specified in the image used in this example (`maven:latest`). Adjust this value according to the image you are actually using in your project.
60 |
--------------------------------------------------------------------------------
/docs/content/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: reproducible build environments
3 | date: 2020-04-13
4 | ---
5 |
6 | # ilo
7 |
8 | > manage reproducible build environments
9 |
10 | `ilo` is a [toolbx](https://containertoolbx.org/) inspired tool to create/manage environments for [reproducible builds](https://reproducible-builds.org/) based on [OCI](https://www.opencontainers.org/) container images.
11 |
12 | ## Features
13 |
14 | ### Reproducible Build Environments
15 |
16 | Thanks to containers, `ilo` can fully encapsulate the necessary tools required to build your project. Therefore, making it easy to reproduce the build output of any project. Custom tooling, a specific version of a compiler, or anything else required to build a project are no longer showstoppers, but rather implementation details.
17 |
18 | ### Per-Project Dependencies
19 |
20 | `ilo` recognizes that lots of projects have their own unique build requirements. Instead of forcing users to install all required tooling into their local system, `ilo` moves all project dependencies into a container. In case you want to clean up your computer, just remove the container image! `ilo` will automatically recreate a build environment for your project the next time you need it.
21 |
22 | ### Teamwork
23 |
24 | Onboarding new team members into big projects with complex build requirements can be a hassle. `ilo`'s container approach reduces the amount of work required to get new members up to speed - install `ilo`, clone your project, and you're good to go. `ilo` supports multiple ways to share immutable build environments with your team in order reproduce a project.
25 |
26 | ### Cross-Platform
27 |
28 | `ilo` is available for [Linux](https://www.kernel.org/), [MacOS](https://www.apple.com/macos/), [Windows](https://www.microsoft.com/en-us/windows), and others. It supports a wide range of runtimes which makes it easy to both add and remove `ilo` from your project. It plays nicely with tools already available on your local system - use your favorite IDE to write code!
29 |
30 | ## Users
31 |
32 | Want to try `ilo` for your project? Take a look at the [usage guide](./usage).
33 |
34 | ## Contributors
35 |
36 | Interested in contributing to `ilo`? Take a look at the [contributor guide](./contributors).
37 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/os/PosixShell.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.os;
7 |
8 | import wtf.metio.ilo.cli.Executables;
9 |
10 | import java.nio.file.Path;
11 | import java.util.regex.Pattern;
12 |
13 | /**
14 | * Support for POSIX compatible shells.
15 | *
16 | * @see Shell Command Language
17 | */
18 | final class PosixShell extends ParameterExpansion {
19 |
20 | private static final String NEW_COMMAND_STYLE = String.format("\\$\\((?<%s>[^)]+)\\)", MATCHER_GROUP_NAME);
21 | private static final String OLD_COMMAND_STYLE = String.format("`(?<%s>[^`]+)`", MATCHER_GROUP_NAME);
22 | private static final String PARAMETER_STYLE = String.format("(?<%s>\\$[a-zA-Z][a-zA-Z0-9_]*)", MATCHER_GROUP_NAME);
23 | private static final String PARAMETER_WITH_BRACES_STYLE = String.format("(?<%s>\\$\\{[a-zA-Z][a-zA-Z0-9_]*})", MATCHER_GROUP_NAME);
24 |
25 | // visible for testing
26 | static final Pattern NEW_COMMAND_PATTERN = Pattern.compile(NEW_COMMAND_STYLE);
27 | static final Pattern OLD_COMMAND_PATTERN = Pattern.compile(OLD_COMMAND_STYLE);
28 | static final Pattern PARAMETER_WITH_BRACES_PATTERN = Pattern.compile(PARAMETER_WITH_BRACES_STYLE);
29 | static final Pattern PARAMETER_PATTERN = Pattern.compile(PARAMETER_STYLE);
30 |
31 | private final Path shellBinary;
32 |
33 | PosixShell(final Path shellBinary) {
34 | this.shellBinary = shellBinary;
35 | }
36 |
37 | @Override
38 | public String substituteCommands(final String value) {
39 | return replace(value,
40 | command -> Executables.runAndReadOutput(shellBinary.toString(), "-c", command),
41 | NEW_COMMAND_PATTERN, OLD_COMMAND_PATTERN);
42 | }
43 |
44 | @Override
45 | public String expandParameters(final String value) {
46 | return replace(expandTilde(value),
47 | parameter -> Executables.runAndReadOutput("/usr/bin/env", shellBinary.toString(), "-c", "printf " + parameter),
48 | PARAMETER_WITH_BRACES_PATTERN, PARAMETER_PATTERN);
49 | }
50 |
51 | private String expandTilde(final String value) {
52 | final var userHome = System.getProperty("user.home");
53 | return value.replace("~", userHome);
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/cli/CommandLifecycle.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.cli;
7 |
8 | import picocli.CommandLine;
9 | import wtf.metio.ilo.model.CliTool;
10 | import wtf.metio.ilo.model.Options;
11 |
12 | import java.util.List;
13 | import java.util.function.BiFunction;
14 | import java.util.stream.IntStream;
15 |
16 | /**
17 | * Utility class that encapsulates the common command lifecycle for 'ilo shell'.
18 | */
19 | public final class CommandLifecycle {
20 |
21 | /**
22 | * The common command lifecycle executes:
23 | *
24 | * - Pull
25 | * - Build
26 | * - Run
27 | * - Cleanup
28 | *
>
29 | *
30 | * @param tool The container tool to use, e.g. podman.
31 | * @param options The options to use for the entire command lifecycle.
32 | * @param executor The executor to use.
33 | * @param The type of the options supplied.
34 | * @param The type of the container tool supplied.
35 | * @return Stream of exit codes, one for each step in the lifecycle.
36 | */
37 | public static > int run(
38 | final CLI tool,
39 | final OPTIONS options,
40 | final BiFunction super List, ? super Boolean, Integer> executor) {
41 | final var pullArguments = tool.pullArguments(options);
42 | final var pullExitCode = executor.apply(pullArguments, options.debug());
43 | if (0 != pullExitCode) {
44 | return pullExitCode;
45 | }
46 | final var buildArguments = tool.buildArguments(options);
47 | final var buildExitCode = executor.apply(buildArguments, options.debug());
48 | if (0 != buildExitCode) {
49 | return buildExitCode;
50 | }
51 | final var runArguments = tool.runArguments(options);
52 | final var runExitCode = executor.apply(runArguments, options.debug());
53 | if (0 != runExitCode) {
54 | return runExitCode;
55 | }
56 | final var cleanupArguments = tool.cleanupArguments(options);
57 | final var cleanupExitCode = executor.apply(cleanupArguments, options.debug());
58 | return IntStream.of(pullExitCode, buildExitCode, runExitCode, cleanupExitCode)
59 | .max().orElse(CommandLine.ExitCode.SOFTWARE);
60 | }
61 |
62 | private CommandLifecycle() {
63 | // utility class
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/docs/content/shell/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ilo shell
3 | date: 2020-04-13
4 | menu: main
5 | weight: 110
6 | ---
7 |
8 | The `ilo shell` command can be used to run a single container either interactively (default) or in non-interactive mode (e.g. for CI builds). It can build an image, mount directories automatically, stop containers, remove images, and customize the build environment according to the needs of your project.
9 |
10 | ```console
11 | # open shell for local builds in default image with default image command
12 | [you@hostname project-dir]$ ilo shell
13 | [root@container project-dir]#
14 |
15 | # use custom image
16 | [you@hostname project-dir]$ ilo shell maven:latest
17 | [root@container project-dir]#
18 |
19 | # use custom command
20 | [you@hostname project-dir]$ ilo shell openjdk:11 jshell
21 | [root@container project-dir]#
22 |
23 | # run command non-interactive
24 | [you@hostname project-dir]$ ilo shell --no-interactive openjdk:11 mvn verify
25 | [you@hostname project-dir]$
26 | ```
27 |
28 | `ilo shell` will delegate most of its work to one of the supported [runtimes](./runtimes). In order to override the default command of your image, specify the command you want to execute just after the image, like this:
29 |
30 | ```console
31 | [you@hostname project-dir]$ ilo shell openjdk:11 /bin/bash
32 | [root@container project-dir]#
33 | ```
34 |
35 | In order to exit the container either use `exit` or hit `Ctrl + d`:
36 |
37 | ```console
38 | [root@container project-dir]# exit
39 | [you@hostname project-dir]$
40 | ```
41 |
42 | Once you have exited the container, `ilo` will automatically stop and remove it. In order to remove the image as well, specify the `--remove-image` flag:
43 |
44 | ```console
45 | [you@hostname project-dir]$ ilo shell --remove-image openjdk:11
46 | [root@container project-dir]# exit
47 | ```
48 |
49 | In order to pull an image first before opening a new shell, use the `--pull` flag like this:
50 |
51 | ```console
52 | [you@hostname project-dir]$ ilo shell --pull openjdk:latest
53 | [root@container project-dir]#
54 | ```
55 |
56 | In case you want to use a local `Containerfile`/`Dockerfile`, use the `--containerfile`/`--dockerfile` flag like this:
57 |
58 | ```console
59 | [you@hostname project-dir]$ ilo shell --containerfile your.containerfile your.image:latest
60 | [root@container project-dir]#
61 | ```
62 |
63 | The resulting image name will be `your.image:latest`. Take a look at all available [options](./options) or use `ilo shell --help` to get a list of all options, and their default values. In order to simplify handling of long command line options, consider using [argument files](../usage/argument-files).
64 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/Ilo.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo;
7 |
8 | import picocli.AutoComplete;
9 | import picocli.CommandLine;
10 | import wtf.metio.ilo.cli.RunCommands;
11 | import wtf.metio.ilo.errors.ExitCodes;
12 | import wtf.metio.ilo.errors.PrintingExceptionHandler;
13 | import wtf.metio.ilo.shell.ShellCommand;
14 | import wtf.metio.ilo.version.VersionProvider;
15 |
16 | import java.nio.file.Paths;
17 | import java.util.Arrays;
18 | import java.util.stream.Stream;
19 |
20 | /**
21 | * Main entry point for ilo - a little tool to manage reproducible build environments
22 | */
23 | @CommandLine.Command(
24 | name = "ilo",
25 | description = "Manage reproducible build environments",
26 | versionProvider = VersionProvider.class,
27 | mixinStandardHelpOptions = true,
28 | showAtFileInUsageHelp = true,
29 | usageHelpAutoWidth = true,
30 | synopsisSubcommandLabel = "COMMAND",
31 | descriptionHeading = "%n",
32 | parameterListHeading = "%n",
33 | optionListHeading = "%nOptions:%n",
34 | commandListHeading = "%nCommands:%n",
35 | subcommands = {
36 | ShellCommand.class,
37 | AutoComplete.GenerateCompletion.class
38 | },
39 | showDefaultValues = true
40 | )
41 | public final class Ilo implements Runnable {
42 |
43 | @CommandLine.Spec
44 | CommandLine.Model.CommandSpec spec;
45 |
46 | public static void main(final String... userInput) {
47 | System.setProperty("picocli.disable.closures", "true");
48 | System.exit(commandLine().execute(allArguments(userInput)));
49 | }
50 |
51 | // visible for testing
52 | static String[] allArguments(final String[] userInput) {
53 | return Stream.concat(runCommands(userInput), Arrays.stream(userInput)).toArray(String[]::new);
54 | }
55 |
56 | // visible for testing
57 | static Stream runCommands(final String[] userInput) {
58 | if (RunCommands.shouldAddRunCommands(userInput)) {
59 | final var currentDir = Paths.get(System.getProperty("user.dir"));
60 | return RunCommands.locate(currentDir);
61 | }
62 | return Stream.empty();
63 | }
64 |
65 | // visible for testing
66 | public static CommandLine commandLine() {
67 | final var commandLine = new CommandLine(new Ilo());
68 | commandLine.setStopAtPositional(true);
69 | commandLine.setCaseInsensitiveEnumValuesAllowed(true);
70 | commandLine.setExecutionExceptionHandler(new PrintingExceptionHandler());
71 | commandLine.setExitCodeExceptionMapper(new ExitCodes());
72 | return commandLine;
73 | }
74 |
75 | @Override
76 | public void run() {
77 | throw new CommandLine.ParameterException(spec.commandLine(), "ERROR: Missing required subcommand" + System.lineSeparator());
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/cli/RunCommands.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.cli;
7 |
8 | import java.nio.file.Files;
9 | import java.nio.file.Path;
10 | import java.util.Arrays;
11 | import java.util.stream.Stream;
12 |
13 | /**
14 | * Support for so-called RC files.
15 | *
16 | * @see Run Commands
17 | * @see Picocli Argument Files
18 | */
19 | public final class RunCommands {
20 |
21 | /**
22 | * Locate run commands on the host machine and prepares them for loading by picocli by prepending a '@' in front of
23 | * the path. This turns them into argument files which are natively supported by picocli.
24 | *
25 | * @param baseDirectory The base directory to use for relative paths.
26 | * @return Stream of run command paths, prepended with '@'.
27 | */
28 | public static Stream locate(final Path baseDirectory) {
29 | if (System.getenv().containsKey(EnvironmentVariables.ILO_RC.name())) {
30 | final var rcFiles = System.getenv().get(EnvironmentVariables.ILO_RC.name());
31 | final var files = rcFiles.split(",");
32 | return asArgumentFiles(Arrays.stream(files).map(String::trim).map(baseDirectory::resolve));
33 | }
34 | return asArgumentFiles(Stream.of(".ilo/ilo.rc", ".ilo.rc").map(baseDirectory::resolve));
35 | }
36 |
37 | private static Stream asArgumentFiles(final Stream extends Path> locations) {
38 | return locations
39 | .filter(Files::isReadable)
40 | .filter(Files::isRegularFile)
41 | .map(Path::toAbsolutePath)
42 | .map(Path::toString)
43 | .map("@"::concat);
44 | }
45 |
46 | /**
47 | * Poor-mans guard to prohibit adding run command files in some 'special' cases, e.g. users wants to see 'help'.
48 | *
49 | * @param args The CLI arguments for ilo itself.
50 | * @return Whether to add run command files or not.
51 | */
52 | public static boolean shouldAddRunCommands(final String[] args) {
53 | final var hasArguments = 0 < args.length;
54 | final var isVersion = (hasArguments && ("-V".equals(args[0]) || "--version".equals(args[0])))
55 | || 1 < args.length && ("-V".equals(args[1]) || "--version".equals(args[1]));
56 | final var isHelp = (hasArguments && ("-h".equals(args[0]) || "--help".equals(args[0])))
57 | || (1 < args.length && ("-h".equals(args[1]) || "--help".equals(args[1])));
58 | final var isCompletion = hasArguments && "generate-completion".equals(args[0]);
59 | final var disableRunCommands = hasArguments && "--no-rc".equals(args[0]);
60 |
61 | return !isVersion && !isHelp && !isCompletion && !disableRunCommands;
62 | }
63 |
64 | private RunCommands() {
65 | // utility class
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/shell/ShellVolumeBehavior.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import wtf.metio.ilo.errors.LocalDirectoryDoesNotExistException;
9 |
10 | import java.io.IOException;
11 | import java.nio.file.Files;
12 | import java.nio.file.Path;
13 | import java.nio.file.Paths;
14 | import java.util.List;
15 |
16 | import static java.util.stream.Collectors.toList;
17 | import static wtf.metio.ilo.utils.Streams.filter;
18 | import static wtf.metio.ilo.utils.Streams.fromList;
19 |
20 | /**
21 | * Controls how the 'ilo shell' command handles missing local mount directories.
22 | */
23 | public enum ShellVolumeBehavior {
24 |
25 | /**
26 | * Automatically create local volume mount directories that do not exist. If local directory cannot be created,
27 | * behave like WARN and remove from --volume list.
28 | */
29 | CREATE {
30 | @Override
31 | boolean handleMissingDirectory(final Path directory) {
32 | try {
33 | if (Files.notExists(directory)) {
34 | Files.createDirectories(directory);
35 | }
36 | return true;
37 | } catch (final IOException exception) {
38 | System.err.println("Could not create directory " + directory.toAbsolutePath() + " because of " + exception.getMessage());
39 | return false;
40 | }
41 | }
42 | },
43 |
44 | /**
45 | * Warn in case local mount directories do not exist remove them from --volume list.
46 | */
47 | WARN {
48 | @Override
49 | boolean handleMissingDirectory(final Path directory) {
50 | if (Files.exists(directory)) {
51 | return true;
52 | }
53 | System.out.println("The local directory " + directory.toAbsolutePath() + " does not exist.");
54 | return false;
55 | }
56 | },
57 |
58 | /**
59 | * Error in case local mount directories do not exist and stop execution.
60 | */
61 | ERROR {
62 | @Override
63 | boolean handleMissingDirectory(final Path directory) {
64 | if (Files.notExists(directory)) {
65 | throw new LocalDirectoryDoesNotExistException(directory);
66 | }
67 | return true;
68 | }
69 | };
70 |
71 | public List handleLocalDirectories(final List volumes) {
72 | return filter(fromList(volumes))
73 | .filter(this::handleLocalDirectory)
74 | .collect(toList());
75 | }
76 |
77 | private boolean handleLocalDirectory(final String volume) {
78 | final var localDirectory = extractLocalPart(volume);
79 | final var localPath = Paths.get(localDirectory);
80 | return handleMissingDirectory(localPath);
81 | }
82 |
83 | // visible for testing
84 | static String extractLocalPart(final String volume) {
85 | return volume.split(":")[0];
86 | }
87 |
88 | abstract boolean handleMissingDirectory(Path directory);
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/docs/content/contributors/first-timer.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: First Timer
3 | date: 2020-04-13
4 | menu:
5 | main:
6 | parent: 'Contributors'
7 | categories:
8 | - Contributors
9 | tags:
10 | - help
11 | ---
12 |
13 | `ilo` is an open source product released under the [0BSD license](https://spdx.org/licenses/0BSD.html). In order to make sure that each contribution is correctly attributed and licensed, the Developer Certificate of Origin (DCO) **MUST** be signed by each contributor with their first commit. In order to do so, simply add a `Signed-off-by` statement at the end of your commit yourself or use `git commit -s` to do that automatically. The DCO can be seen below or at https://developercertificate.org/
14 |
15 | ```
16 | Developer's Certificate of Origin 1.1
17 |
18 | By making a contribution to this project, I certify that:
19 |
20 | (a) The contribution was created in whole or in part by me and I
21 | have the right to submit it under the open source license
22 | indicated in the file; or
23 |
24 | (b) The contribution is based upon previous work that, to the
25 | best of my knowledge, is covered under an appropriate open
26 | source license and I have the right under that license to
27 | submit that work with modifications, whether created in whole
28 | or in part by me, under the same open source license (unless
29 | I am permitted to submit under a different license), as
30 | Indicated in the file; or
31 |
32 | (c) The contribution was provided directly to me by some other
33 | person who certified (a), (b) or (c) and I have not modified
34 | it.
35 |
36 | (d) I understand and agree that this project and the contribution
37 | are public and that a record of the contribution (including
38 | all personal information I submit with it, including my
39 | sign-off) is maintained indefinitely and may be redistributed
40 | consistent with this project or the open source license(s)
41 | involved.
42 | ```
43 |
44 | ## Metadata
45 |
46 | Every contributor **MAY** add/remove their metadata to the list of contributors at any time. Simply add a file called `.yaml` in the [contributors directory](https://github.com/metio/ilo/tree/main/docs/data/contributors) with the following properties:
47 |
48 | ```yaml
49 | id: '' # should match file name (required)
50 | title: '' # used by FOAF/humans.txt (optional)
51 | first_name: '' # used by FOAF/humans.txt (optional)
52 | last_name: '' # used by FOAF/humans.txt (optional)
53 | email: '' # used by FOAF (optional)
54 | website: '' # used by FOAF/humans.txt (optional)
55 | ```
56 |
57 | Metadata is currently used in three places:
58 |
59 | 1. To generate a [humans.txt](https://humanstxt.org/) file of [all contributors](https://ilo.projects.metio.wtf/humans.txt).
60 | 2. To generate a [FOAF](http://xmlns.com/foaf/spec/) for the [entire project](https://ilo.projects.metio.wtf/foaf.rdf).
61 |
--------------------------------------------------------------------------------
/docs/content/usage/argument-files.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Argument Files
3 | date: 2020-04-13
4 | menu:
5 | main:
6 | parent: usage
7 | identifier: usage_arg_files
8 | categories:
9 | - usage
10 | tags:
11 | - argument files
12 | ---
13 |
14 | In order to share the same options/commands across your team, `ilo` supports argument files which contain the options for your project, e.g. which image you are using. Argument files are just plain text files and both name and location can be chosen at will. In order to use an argument file, you have to add **@** in front of the file name: `ilo @file-name`.
15 |
16 | ```console
17 | # write argument file
18 | $ cat some/folder/your-arguments
19 | shell
20 | node:latest
21 | /bin/bash
22 |
23 | # use argument file
24 | $ ilo @some/folder/your-arguments
25 | ```
26 |
27 | The argument file in the above example specified all commands and options on a new line, however you could write them all in a single line (or a mixture of both) as well:
28 |
29 | ```console
30 | # write argument file
31 | $ cat some/other/your-arguments
32 | shell node:latest /bin/bash
33 |
34 | # write argument file
35 | $ cat some/more/of/your-arguments
36 | shell
37 | node:latest /bin/bash
38 |
39 | # use argument file
40 | $ ilo @some/other/your-arguments
41 | $ ilo @some/more/of/your-arguments
42 | ```
43 |
44 | **Important**: In case your option contains a whitespace, you have to either put the entire option with its value in single/double quotes or use a whitespace between option and value like this:
45 |
46 | ```console
47 | # quote the entire option
48 | "--runtime-option=some option here"
49 |
50 | # quote the value
51 | --runtime-option "some option here"
52 |
53 | # THIS WON'T WORK
54 | --runtime-option="some option here"
55 | ```
56 |
57 | You can use multiple arguments files which are evaluated in-order, e.g like this:
58 |
59 | ```console
60 | $ ilo @first @second
61 | ```
62 |
63 | You can mix argument files with regular CLI options as well:
64 |
65 | ```console
66 | $ ilo shell @default-shell openjdk:11
67 | ```
68 |
69 | The argument file used by `ilo` developers can be seen [here](https://github.com/metio/ilo/blob/main/dev/env) and is used by calling `ilo @dev/env`.
70 |
71 | ## RC Files
72 |
73 | In order to simplify/automate its usage, `ilo` will look for [run command](https://en.wikipedia.org/wiki/Run_commands) files in the following locations:
74 |
75 | 1. `.ilo/ilo.rc`
76 | 2. `.ilo.rc`
77 |
78 | **Each** file found will be added in-order as an argument file to your invocation of `ilo` **before** any other options you specify in your terminal. You can change the locations to check by specifying the `ILO_RC` environment variable. Multiple locations can be given by separating them with a comma like this:
79 |
80 | ```console
81 | $ export ILO_RC=some-file.rc,another-one.rc
82 | $ ilo ...
83 | ```
84 |
85 | In order to disable loading `.rc` files entirely, specify `--no-rc` in the command line before the actual `ilo` subcommand, like this:
86 |
87 | ```console
88 | # do not load .rc files
89 | $ ilo --no-rc shell ...
90 | $ ilo --no-rc @some-argument-file ...
91 | ```
92 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/acceptance/IloAcceptanceTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.acceptance;
7 |
8 | import org.junit.jupiter.api.Disabled;
9 | import org.junit.jupiter.api.DisplayName;
10 | import org.junit.jupiter.api.Test;
11 | import org.junit.jupiter.params.ParameterizedTest;
12 | import org.junit.jupiter.params.provider.ValueSource;
13 | import wtf.metio.ilo.shell.ShellRuntime;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 |
17 | @DisplayName("Ilo")
18 | class IloAcceptanceTest extends CLI_TCK {
19 |
20 | @Test
21 | @DisplayName("select runtime automatically by default")
22 | void shouldDefaultToAutoRuntimeSelection() {
23 | final var shell = parseShellCommand("shell");
24 | assertNull(shell.options.runtime);
25 | }
26 |
27 | @DisplayName("allow to specify runtime")
28 | @ParameterizedTest
29 | @ValueSource(strings = {"podman", "docker", "p", "d"})
30 | void shouldAllowToSpecifyRuntime(final String runtime) {
31 | final var shell = parseShellCommand("shell", "--runtime", runtime);
32 | assertEquals(ShellRuntime.fromAlias(runtime), shell.options.runtime);
33 | }
34 |
35 | @Test
36 | @DisplayName("debug is disabled by default")
37 | void shouldDisableDebugByDefault() {
38 | final var shell = parseShellCommand("shell");
39 | assertFalse(shell.options.debug);
40 | }
41 |
42 | @Test
43 | @DisplayName("allow to enable debug")
44 | void shouldAllowToEnableDebug() {
45 | final var shell = parseShellCommand("shell", "--debug");
46 | assertTrue(shell.options.debug);
47 | }
48 |
49 | @Test
50 | @DisplayName("allow to disable debug")
51 | void shouldAllowToDisableDebug() {
52 | final var shell = parseShellCommand("shell", "--debug=false");
53 | assertFalse(shell.options.debug);
54 | }
55 |
56 | @Test
57 | @DisplayName("interactive mode is enabled by default")
58 | void shouldEnableInteractiveModeByDefault() {
59 | final var shell = parseShellCommand("shell");
60 | assertTrue(shell.options.interactive);
61 | }
62 |
63 | @Test
64 | @Disabled("negate does not work from tests?")
65 | @DisplayName("interactive mode can be negated")
66 | void shouldAllowToNegateInteractiveMode() {
67 | final var shell = parseShellCommand("shell", "--no-interactive");
68 | assertFalse(shell.options.interactive);
69 | }
70 |
71 | @Test
72 | @DisplayName("interactive mode can be disabled")
73 | void shouldAllowToDisableInteractiveMode() {
74 | final var shell = parseShellCommand("shell", "--interactive=false");
75 | assertFalse(shell.options.interactive);
76 | }
77 |
78 | @Test
79 | @DisplayName("project directory should be mounted by default")
80 | void shouldMountProjectDirectoryByDefault() {
81 | final var shell = parseShellCommand("shell");
82 | assertTrue(shell.options.mountProjectDir);
83 | }
84 |
85 | @Test
86 | @DisplayName("mounting the project directory can be disabled")
87 | void shouldAllowToDisableProjectDirectoryMounting() {
88 | final var shell = parseShellCommand("shell", "--mount-project-dir=false");
89 | assertFalse(shell.options.mountProjectDir);
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/shell/DockerLike.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import wtf.metio.ilo.os.OSSupport;
9 | import wtf.metio.ilo.utils.Strings;
10 |
11 | import java.util.List;
12 | import java.util.Optional;
13 |
14 | import static java.util.stream.Stream.of;
15 | import static wtf.metio.ilo.utils.Streams.*;
16 |
17 | abstract class DockerLike implements ShellCLI {
18 |
19 | @Override
20 | public final List pullArguments(final ShellOptions options) {
21 | if (options.pull && Strings.isBlank(options.containerfile)) {
22 | return flatten(
23 | of(name()),
24 | fromList(OSSupport.expand(options.runtimeOptions)),
25 | of("pull"),
26 | fromList(OSSupport.expand(options.runtimePullOptions)),
27 | of(OSSupport.expand(options.image)));
28 | }
29 | return List.of();
30 | }
31 |
32 | @Override
33 | public final List buildArguments(final ShellOptions options) {
34 | if (Strings.isNotBlank(options.containerfile)) {
35 | return flatten(
36 | of(name()),
37 | fromList(OSSupport.expand(options.runtimeOptions)),
38 | of("build", "--file", options.containerfile),
39 | fromList(OSSupport.expand(options.runtimeBuildOptions)),
40 | maybe(options.pull, "--pull"),
41 | of("--tag", OSSupport.expand(options.image)),
42 | of(OSSupport.expand(options.context)));
43 | }
44 | return List.of();
45 | }
46 |
47 | @Override
48 | public final List runArguments(final ShellOptions options) {
49 | final var currentDir = System.getProperty("user.dir");
50 | final var workingDir = Optional.ofNullable(options.workingDir)
51 | .filter(Strings::isNotBlank)
52 | .orElse(currentDir);
53 | final var projectDir = maybe(options.mountProjectDir,
54 | "--volume", currentDir + ":" + workingDir + ":z");
55 | return flatten(
56 | of(name()),
57 | fromList(OSSupport.expand(options.runtimeOptions)),
58 | of("run", "--rm"),
59 | fromList(OSSupport.expand(options.runtimeRunOptions)),
60 | projectDir,
61 | of("--workdir", workingDir),
62 | maybe(options.interactive, "--interactive", "--tty"),
63 | of("--env", "ILO_CONTAINER=true"),
64 | withPrefix("--env", OSSupport.expand(options.variables)),
65 | optional("--hostname", OSSupport.expand(options.hostname)),
66 | withPrefix("--publish", OSSupport.expand(options.ports)),
67 | withPrefix("--volume", options.missingVolumes.handleLocalDirectories(OSSupport.expand(options.volumes))),
68 | of(OSSupport.expand(options.image)),
69 | fromList(OSSupport.expand(options.commands)));
70 | }
71 |
72 | @Override
73 | public final List cleanupArguments(final ShellOptions options) {
74 | if (options.removeImage) {
75 | return flatten(
76 | of(name()),
77 | fromList(OSSupport.expand(options.runtimeOptions)),
78 | of("rmi"),
79 | fromList(OSSupport.expand(options.runtimeCleanupOptions)),
80 | of(OSSupport.expand(options.image)));
81 | }
82 | return List.of();
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/docs/themes/metio/static/images/matrix.svg:
--------------------------------------------------------------------------------
1 |
2 |
64 |
--------------------------------------------------------------------------------
/docs/themes/metio/assets/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 | html {
3 | line-height: 1.15; /* 1 */
4 | -webkit-text-size-adjust: 100%; /* 2 */
5 | }
6 |
7 | body {
8 | margin: 0;
9 | }
10 |
11 | main {
12 | display: block;
13 | }
14 |
15 | h1 {
16 | font-size: 2em;
17 | margin: 0.67em 0;
18 | }
19 |
20 | hr {
21 | box-sizing: content-box; /* 1 */
22 | height: 0; /* 1 */
23 | overflow: visible; /* 2 */
24 | }
25 |
26 | pre {
27 | font-family: monospace, monospace; /* 1 */
28 | font-size: 1em; /* 2 */
29 | }
30 |
31 | a {
32 | background-color: transparent;
33 | }
34 |
35 | abbr[title] {
36 | border-bottom: none; /* 1 */
37 | text-decoration: underline; /* 2 */
38 | text-decoration: underline dotted; /* 2 */
39 | }
40 |
41 | b,
42 | strong {
43 | font-weight: bolder;
44 | }
45 |
46 | code,
47 | kbd,
48 | samp {
49 | font-family: monospace, monospace; /* 1 */
50 | font-size: 1em; /* 2 */
51 | }
52 |
53 | small {
54 | font-size: 80%;
55 | }
56 |
57 | sub,
58 | sup {
59 | font-size: 75%;
60 | line-height: 0;
61 | position: relative;
62 | vertical-align: baseline;
63 | }
64 |
65 | sub {
66 | bottom: -0.25em;
67 | }
68 |
69 | sup {
70 | top: -0.5em;
71 | }
72 |
73 | img {
74 | border-style: none;
75 | }
76 |
77 | button,
78 | input,
79 | optgroup,
80 | select,
81 | textarea {
82 | font-family: inherit; /* 1 */
83 | font-size: 100%; /* 1 */
84 | line-height: 1.15; /* 1 */
85 | margin: 0; /* 2 */
86 | }
87 |
88 | button,
89 | input { /* 1 */
90 | overflow: visible;
91 | }
92 |
93 | button,
94 | select { /* 1 */
95 | text-transform: none;
96 | }
97 |
98 | button,
99 | [type="button"],
100 | [type="reset"],
101 | [type="submit"] {
102 | -webkit-appearance: button;
103 | }
104 |
105 | button::-moz-focus-inner,
106 | [type="button"]::-moz-focus-inner,
107 | [type="reset"]::-moz-focus-inner,
108 | [type="submit"]::-moz-focus-inner {
109 | border-style: none;
110 | padding: 0;
111 | }
112 |
113 | button:-moz-focusring,
114 | [type="button"]:-moz-focusring,
115 | [type="reset"]:-moz-focusring,
116 | [type="submit"]:-moz-focusring {
117 | outline: 1px dotted ButtonText;
118 | }
119 |
120 | fieldset {
121 | padding: 0.35em 0.75em 0.625em;
122 | }
123 |
124 | legend {
125 | box-sizing: border-box; /* 1 */
126 | color: inherit; /* 2 */
127 | display: table; /* 1 */
128 | max-width: 100%; /* 1 */
129 | padding: 0; /* 3 */
130 | white-space: normal; /* 1 */
131 | }
132 |
133 | progress {
134 | vertical-align: baseline;
135 | }
136 |
137 | textarea {
138 | overflow: auto;
139 | }
140 |
141 | [type="checkbox"],
142 | [type="radio"] {
143 | box-sizing: border-box; /* 1 */
144 | padding: 0; /* 2 */
145 | }
146 |
147 | [type="number"]::-webkit-inner-spin-button,
148 | [type="number"]::-webkit-outer-spin-button {
149 | height: auto;
150 | }
151 |
152 | [type="search"] {
153 | -webkit-appearance: textfield; /* 1 */
154 | outline-offset: -2px; /* 2 */
155 | }
156 |
157 | [type="search"]::-webkit-search-decoration {
158 | -webkit-appearance: none;
159 | }
160 |
161 | ::-webkit-file-upload-button {
162 | -webkit-appearance: button; /* 1 */
163 | font: inherit; /* 2 */
164 | }
165 |
166 | details {
167 | display: block;
168 | }
169 |
170 | summary {
171 | display: list-item;
172 | }
173 |
174 | template {
175 | display: none;
176 | }
177 |
178 | [hidden] {
179 | display: none;
180 | }
181 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/cli/Executables.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.cli;
7 |
8 | import wtf.metio.ilo.errors.*;
9 |
10 | import java.io.BufferedReader;
11 | import java.io.File;
12 | import java.io.IOException;
13 | import java.io.InputStreamReader;
14 | import java.nio.file.Files;
15 | import java.nio.file.Path;
16 | import java.nio.file.Paths;
17 | import java.util.List;
18 | import java.util.Optional;
19 | import java.util.regex.Pattern;
20 | import java.util.stream.Stream;
21 |
22 | /**
23 | * Utility class that interacts with executables found on the host machine.
24 | */
25 | public final class Executables {
26 |
27 | /**
28 | * Resolves a tool by its name from the current $PATH. To match shell behavior, the first match will be returned.
29 | * Thus make sure to order your $PATH so that your preferred location will be picked first.
30 | *
31 | * @param tool The name of the tool to look up.
32 | * @return The path to the tool or an empty optional.
33 | */
34 | public static Optional of(final String tool) {
35 | return allPaths().map(path -> path.resolve(tool))
36 | .filter(Executables::canExecute)
37 | .findFirst();
38 | }
39 |
40 | // visible for testing
41 | static Stream allPaths() {
42 | return Stream.of(System.getenv("PATH")
43 | .split(Pattern.quote(File.pathSeparator)))
44 | .map(Paths::get);
45 | }
46 |
47 | // visible for testing
48 | static boolean canExecute(final Path binary) {
49 | return Files.exists(binary) && Files.isExecutable(binary);
50 | }
51 |
52 | public static int runAndWaitForExit(final List arguments, final boolean debug) {
53 | if (null == arguments || arguments.isEmpty()) {
54 | return 0;
55 | }
56 | if (debug) {
57 | System.out.println("ilo executes: " + String.join(" ", arguments));
58 | }
59 | try {
60 | return new ProcessBuilder(arguments).inheritIO().start().waitFor();
61 | } catch (final InterruptedException exception) {
62 | throw new UnexpectedInterruptionException(exception);
63 | } catch (final UnsupportedOperationException exception) {
64 | throw new OperatingSystemNotSupportedException(exception);
65 | } catch (final NullPointerException exception) {
66 | throw new CommandListContainsNullException(exception, arguments);
67 | } catch (final IndexOutOfBoundsException exception) {
68 | throw new CommandListIsEmptyException(exception);
69 | } catch (final SecurityException exception) {
70 | throw new SecurityManagerDeniesAccessException(exception);
71 | } catch (final IOException exception) {
72 | throw new RuntimeIOException(exception);
73 | }
74 | }
75 |
76 | public static String runAndReadOutput(final String... arguments) {
77 | try {
78 | final var processBuilder = new ProcessBuilder(arguments);
79 | final var process = processBuilder.start();
80 | try (final var reader = new InputStreamReader(process.getInputStream());
81 | final var buffer = new BufferedReader(reader)) {
82 | final var builder = new StringBuilder();
83 | String line;
84 | while (null != (line = buffer.readLine())) {
85 | builder.append(line);
86 | builder.append(System.lineSeparator());
87 | }
88 | return builder.toString().strip();
89 | }
90 | } catch (final IOException exception) {
91 | throw new RuntimeIOException(exception);
92 | }
93 | }
94 |
95 | private Executables() {
96 | // utility class
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/cli/ExecutablesTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.cli;
7 |
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.condition.EnabledOnOs;
11 | import org.junit.jupiter.api.condition.OS;
12 | import org.junit.jupiter.api.extension.ExtendWith;
13 | import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
14 | import uk.org.webcompere.systemstubs.stream.SystemOut;
15 |
16 | import java.nio.file.Files;
17 | import java.nio.file.Paths;
18 | import java.util.List;
19 |
20 | import static org.junit.jupiter.api.Assertions.*;
21 |
22 | @ExtendWith(SystemStubsExtension.class)
23 | class ExecutablesTest {
24 |
25 | @Test
26 | @EnabledOnOs({OS.LINUX, OS.MAC})
27 | @DisplayName("Should detect tool in PATH")
28 | void shouldDetectToolInPath() {
29 | // given
30 | final var tool = "ls";
31 |
32 | // when
33 | final var path = Executables.of(tool);
34 |
35 | // then
36 | assertTrue(path.isPresent());
37 | }
38 |
39 | @Test
40 | @DisplayName("Should detect missing tool in PATH")
41 | void shouldHandleMissingTool() {
42 | // given
43 | final var tool = "fgsdfgsdlgdjlgkjsdlfgjskdfgjsldfjgdflg";
44 |
45 | // when
46 | final var path = Executables.of(tool);
47 |
48 | // then
49 | assertTrue(path.isEmpty());
50 | }
51 |
52 | @Test
53 | @EnabledOnOs({OS.LINUX, OS.MAC})
54 | void shouldBeAbleToExecuteLs() {
55 | // given
56 | final var tool = Executables.allPaths()
57 | .map(path -> path.resolve("ls"))
58 | .filter(Files::exists)
59 | .findFirst()
60 | .orElseThrow();
61 |
62 | // when
63 | final var canExecute = Executables.canExecute(tool);
64 |
65 | // then
66 | assertTrue(canExecute);
67 | }
68 |
69 | @Test
70 | void shouldNotBeAbleToExecuteMissing() {
71 | // given
72 | final var tool = Paths.get("asdfasdfasadaggfksdjfgsdfglsdfglsfg");
73 |
74 | // when
75 | final var canExecute = Executables.canExecute(tool);
76 |
77 | // then
78 | assertFalse(canExecute);
79 | }
80 |
81 | @Test
82 | @EnabledOnOs({OS.LINUX, OS.MAC})
83 | void shouldNotBeAbleToExecuteTextFile() {
84 | // given
85 | final var tool = Paths.get("/etc/os-release");
86 |
87 | // when
88 | final var canExecute = Executables.canExecute(tool);
89 |
90 | // then
91 | assertFalse(canExecute);
92 | }
93 |
94 | @Test
95 | @EnabledOnOs({OS.LINUX, OS.MAC})
96 | @DisplayName("waits until tool exits")
97 | void shouldWaitForExit() {
98 | // given
99 | final var tool = "ls";
100 |
101 | // when
102 | final var exitCode = Executables.runAndWaitForExit(List.of(tool), false);
103 |
104 | // then
105 | assertEquals(0, exitCode);
106 | }
107 |
108 | @Test
109 | @EnabledOnOs({OS.LINUX, OS.MAC})
110 | @DisplayName("returns exit code on failures")
111 | void shouldReturnNonZeroExitCode() {
112 | // given
113 | final var arguments = List.of("ls", "--unknown");
114 |
115 | // when
116 | final var exitCode = Executables.runAndWaitForExit(arguments, false);
117 |
118 | // then
119 | assertTrue(0 < exitCode);
120 | }
121 |
122 | @Test
123 | @EnabledOnOs({OS.LINUX, OS.MAC})
124 | @DisplayName("writes debug message to system.out")
125 | void shouldWriteDebugMessageToSystemOut(final SystemOut systemOut) {
126 | // given
127 | final var tool = "ls";
128 |
129 | // when
130 | Executables.runAndWaitForExit(List.of(tool), true);
131 |
132 | // then
133 | assertEquals("ilo executes: ls\n", systemOut.getText());
134 | }
135 |
136 | @Test
137 | @DisplayName("ignores empty lists")
138 | void shouldNoExecuteEmptyList() {
139 | // given
140 | final List arguments = List.of();
141 |
142 | // when
143 | final var exitCode = Executables.runAndWaitForExit(arguments, false);
144 |
145 | // then
146 | assertEquals(0, exitCode);
147 | }
148 |
149 | @Test
150 | @DisplayName("ignores null lists")
151 | void shouldNoExecuteNullList() {
152 | // given
153 | final List arguments = null;
154 |
155 | // when
156 | final var exitCode = Executables.runAndWaitForExit(arguments, false);
157 |
158 | // then
159 | assertEquals(0, exitCode);
160 | }
161 |
162 | }
163 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/shell/ShellVolumeBehaviorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import com.google.common.jimfs.Configuration;
9 | import com.google.common.jimfs.Jimfs;
10 | import org.junit.jupiter.api.DisplayName;
11 | import org.junit.jupiter.api.Test;
12 | import org.junit.jupiter.params.ParameterizedTest;
13 | import org.junit.jupiter.params.provider.ValueSource;
14 | import wtf.metio.ilo.errors.LocalDirectoryDoesNotExistException;
15 |
16 | import java.io.IOException;
17 | import java.nio.file.Files;
18 | import java.nio.file.Path;
19 |
20 | import static org.junit.jupiter.api.Assertions.*;
21 |
22 | @DisplayName("ShellVolumeBehavior")
23 | class ShellVolumeBehaviorTest {
24 |
25 | @ParameterizedTest
26 | @DisplayName("defines behavior")
27 | @ValueSource(strings = {
28 | "CREATE",
29 | "WARN",
30 | "ERROR"
31 | })
32 | void shouldHaveBehavior(final String runtime) {
33 | assertNotNull(ShellVolumeBehavior.valueOf(runtime));
34 | }
35 |
36 | @Test
37 | @DisplayName("CREATE: handle missing")
38 | void shouldCreateMissingDirectory() {
39 | final var directory = localDirectory("/missing");
40 | final var ok = ShellVolumeBehavior.CREATE.handleMissingDirectory(directory);
41 | assertAll(
42 | () -> assertTrue(ok, "Missing directory could not be created"),
43 | () -> assertTrue(Files.exists(directory), "Directory was not actually created"));
44 | }
45 |
46 | @Test
47 | @DisplayName("CREATE: handle existing")
48 | void shouldIgnoreExistingDirectory() throws IOException {
49 | final var directory = localDirectory("/existing");
50 | Files.createDirectory(directory);
51 | final var ok = ShellVolumeBehavior.CREATE.handleMissingDirectory(directory);
52 | assertTrue(ok, "Existing directory was not ignored");
53 | }
54 |
55 | @Test
56 | @DisplayName("WARN: handle missing")
57 | void shouldWarnOnMissingDirectory() {
58 | final var directory = localDirectory("/missing");
59 | final var ok = ShellVolumeBehavior.WARN.handleMissingDirectory(directory);
60 | assertAll(
61 | () -> assertFalse(ok, "No warning for missing directory"),
62 | () -> assertTrue(Files.notExists(directory), "Directory was actually created"));
63 | }
64 |
65 | @Test
66 | @DisplayName("WARN: handle existing")
67 | void shouldIgnoreExistingDirectoryWarn() throws IOException {
68 | final var directory = localDirectory("/existing");
69 | Files.createDirectory(directory);
70 | final var ok = ShellVolumeBehavior.WARN.handleMissingDirectory(directory);
71 | assertTrue(ok, "Existing directory was not ignored");
72 | }
73 |
74 | @Test
75 | @DisplayName("ERROR: handle missing")
76 | void shouldThrowOnMissingDirectory() {
77 | final var directory = localDirectory("/missing");
78 | assertAll(
79 | () -> assertThrows(LocalDirectoryDoesNotExistException.class,
80 | () -> ShellVolumeBehavior.ERROR.handleMissingDirectory(directory)),
81 | () -> assertTrue(Files.notExists(directory), "Directory was actually created"));
82 | }
83 |
84 | @Test
85 | @DisplayName("ERROR: handle existing")
86 | void shouldIgnoreExistingDirectoryError() throws IOException {
87 | final var directory = localDirectory("/existing");
88 | Files.createDirectory(directory);
89 | final var ok = ShellVolumeBehavior.ERROR.handleMissingDirectory(directory);
90 | assertTrue(ok, "Existing directory was not ignored");
91 | }
92 |
93 | @Test
94 | @DisplayName("extract local directory")
95 | void shouldExtractLocalDirectory() {
96 | final var mount = "/local/directory:/container/directory";
97 | final var localPart = ShellVolumeBehavior.extractLocalPart(mount);
98 | assertEquals("/local/directory", localPart);
99 | }
100 |
101 | @Test
102 | @DisplayName("extract local directory in a mount directive using a SEL label")
103 | void shouldExtractLocalDirectoryWithSelLabel() {
104 | final var mount = "/local/directory:/container/directory:Z";
105 | final var localPart = ShellVolumeBehavior.extractLocalPart(mount);
106 | assertEquals("/local/directory", localPart);
107 | }
108 |
109 | @Test
110 | @DisplayName("extract local directory in a mount directive without a container path")
111 | void shouldExtractLocalDirectoryWithoutContainerPath() {
112 | final var mount = "/local/directory";
113 | final var localPart = ShellVolumeBehavior.extractLocalPart(mount);
114 | assertEquals("/local/directory", localPart);
115 | }
116 |
117 | private Path localDirectory(final String directory) {
118 | final var fs = Jimfs.newFileSystem(Configuration.unix());
119 | return fs.getPath(directory);
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/os/PowerShellTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.os;
7 |
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.DisplayName;
10 | import org.junit.jupiter.api.Nested;
11 | import org.junit.jupiter.api.Test;
12 | import org.junit.jupiter.api.condition.EnabledOnOs;
13 | import org.junit.jupiter.api.condition.OS;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 | import static wtf.metio.ilo.os.ParameterExpansion.MATCHER_GROUP_NAME;
17 |
18 | @DisplayName("PowerShell")
19 | class PowerShellTest {
20 |
21 | @Nested
22 | @DisplayName("regex")
23 | class Regex {
24 |
25 | @Test
26 | @DisplayName("regex for command")
27 | void regexMatchesCommand() {
28 | final var matcher = PowerShell.COMMAND_PATTERN.matcher("$(some-command --with-option)");
29 | assertAll("new style",
30 | () -> assertTrue(matcher.find(), "matches"),
31 | () -> assertEquals("some-command --with-option", matcher.group(MATCHER_GROUP_NAME), "extraction"));
32 | }
33 |
34 | @Test
35 | @DisplayName("regex for commands")
36 | void regexMatchesCommands() {
37 | final var matcher = PowerShell.COMMAND_PATTERN.matcher("$(some-command --with-option):$(other --option)");
38 | assertAll("new style",
39 | () -> assertTrue(matcher.find(), "first match"),
40 | () -> assertEquals("some-command --with-option", matcher.group(MATCHER_GROUP_NAME), "first extraction"),
41 | () -> assertTrue(matcher.find(), "second match"),
42 | () -> assertEquals("other --option", matcher.group(MATCHER_GROUP_NAME), "second extraction"));
43 | }
44 |
45 | @Test
46 | @DisplayName("regex for parameter")
47 | void regexMatchesParameter() {
48 | final var matcher = PowerShell.PARAMETER_PATTERN.matcher("$HOME");
49 | assertAll("new style",
50 | () -> assertTrue(matcher.find(), "matches"),
51 | () -> assertEquals("$HOME", matcher.group(MATCHER_GROUP_NAME), "extraction"));
52 | }
53 |
54 | @Test
55 | @DisplayName("regex for parameters")
56 | void regexMatchesParameters() {
57 | final var matcher = PowerShell.PARAMETER_PATTERN.matcher("$HOME:$OTHER");
58 | assertAll("new style",
59 | () -> assertTrue(matcher.find(), "first matches"),
60 | () -> assertEquals("$HOME", matcher.group(MATCHER_GROUP_NAME), "first extraction"),
61 | () -> assertTrue(matcher.find(), "second matches"),
62 | () -> assertEquals("$OTHER", matcher.group(MATCHER_GROUP_NAME), "second extraction"));
63 | }
64 |
65 | }
66 |
67 | @Nested
68 | @DisplayName("expansion")
69 | @EnabledOnOs({OS.WINDOWS})
70 | class Expansion {
71 |
72 | private ParameterExpansion powerShell;
73 |
74 | @BeforeEach
75 | void setUp() {
76 | powerShell = OSSupport.powerShell().orElseThrow();
77 | }
78 |
79 | @Test
80 | @DisplayName("replaces parameter")
81 | void replacesParameter() {
82 | final var result = powerShell.replace("$HOME:abc", input -> "test", PowerShell.PARAMETER_PATTERN);
83 | assertEquals("test:abc", result);
84 | }
85 |
86 | @Test
87 | @DisplayName("replaces command")
88 | void replacesCommandWithNewStyle() {
89 | final var result = powerShell.replace("$(id -u):abc", input -> "test", PowerShell.COMMAND_PATTERN);
90 | assertEquals("test:abc", result);
91 | }
92 |
93 | @Test
94 | @DisplayName("replaces commands")
95 | void replacesCommandsWithNewStyle() {
96 | final var result = powerShell.replace("$(id -u):$(id -u)", input -> "test", PowerShell.COMMAND_PATTERN);
97 | assertEquals("test:test", result);
98 | }
99 |
100 | @Test
101 | @DisplayName("keeps constants as-is in parameters")
102 | void keepConstantsInParameters() {
103 | assertEquals("1000:1000", powerShell.expandParameters("1000:1000"));
104 | }
105 |
106 | @Test
107 | @DisplayName("keeps constants as-is in commands")
108 | void keepConstantsInCommands() {
109 | assertEquals("1000:1000", powerShell.substituteCommands("1000:1000"));
110 | }
111 |
112 | @Test
113 | @DisplayName("substitutes commands with their results")
114 | void substituteCommands() {
115 | assertEquals("hello:world", powerShell.substituteCommands("$(echo hello):$(echo world)"));
116 | }
117 |
118 | @Test
119 | @DisplayName("substitutes command with its result and keeps constant")
120 | void substituteCommandAndKeepConstant() {
121 | assertEquals("hello:1234", powerShell.substituteCommands("$(echo hello):1234"));
122 | }
123 |
124 | @Test
125 | @DisplayName("substitutes command with its result and keeps constant")
126 | void keepConstantAndSubstituteCommand() {
127 | assertEquals("1234:world", powerShell.substituteCommands("1234:$(echo world)"));
128 | }
129 |
130 | }
131 |
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/java/wtf/metio/ilo/shell/ShellOptions.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo.shell;
7 |
8 | import picocli.CommandLine;
9 | import wtf.metio.ilo.model.Options;
10 |
11 | import java.util.List;
12 |
13 | public final class ShellOptions implements Options {
14 |
15 | @CommandLine.Option(
16 | names = {"--runtime"},
17 | description = "Specify the runtime to use. If none is specified, use auto-selection.",
18 | converter = ShellRuntimeConverter.class
19 | )
20 | public ShellRuntime runtime;
21 |
22 | @CommandLine.Option(
23 | names = {"--debug"},
24 | description = "Show additional debug information."
25 | )
26 | public boolean debug;
27 |
28 | @CommandLine.Option(
29 | names = {"--pull"},
30 | description = "Pull image before opening shell."
31 | )
32 | public boolean pull;
33 |
34 | @CommandLine.Option(
35 | names = {"--interactive"},
36 | description = "Open interactive shell or just run a single command.",
37 | defaultValue = "true",
38 | fallbackValue = "true",
39 | negatable = true
40 | )
41 | public boolean interactive;
42 |
43 | @CommandLine.Option(
44 | names = {"--mount-project-dir"},
45 | description = "Mount the project directory into the running container. Container path will be the same as --working-dir.",
46 | defaultValue = "true",
47 | fallbackValue = "true",
48 | negatable = true
49 | )
50 | public boolean mountProjectDir;
51 |
52 | @CommandLine.Option(
53 | names = {"--working-dir"},
54 | description = "The directory in the container to use. If not specified, defaults to the current directory."
55 | )
56 | public String workingDir;
57 |
58 | @CommandLine.Option(
59 | names = {"--containerfile", "--dockerfile"},
60 | description = "The Containerfile to use."
61 | )
62 | public String containerfile;
63 |
64 | @CommandLine.Option(
65 | names = {"--context"},
66 | description = "The context to use when building an image.",
67 | defaultValue = "."
68 | )
69 | public String context;
70 |
71 | @CommandLine.Option(
72 | names = {"--hostname"},
73 | description = "The hostname of the running container."
74 | )
75 | public String hostname;
76 |
77 | @CommandLine.Option(
78 | names = {"--remove-image"},
79 | description = "Remove image after closing the shell."
80 | )
81 | public boolean removeImage;
82 |
83 | @CommandLine.Option(
84 | names = {"--runtime-option"},
85 | description = "Options for the selected runtime itself."
86 | )
87 | public List runtimeOptions;
88 |
89 | @CommandLine.Option(
90 | names = {"--runtime-pull-option"},
91 | description = "Options for the pull command of the selected runtime."
92 | )
93 | public List runtimePullOptions;
94 |
95 | @CommandLine.Option(
96 | names = {"--runtime-build-option"},
97 | description = "Options for the build command of the selected runtime."
98 | )
99 | public List runtimeBuildOptions;
100 |
101 | @CommandLine.Option(
102 | names = {"--runtime-run-option"},
103 | description = "Options for the run command of the selected runtime."
104 | )
105 | public List runtimeRunOptions;
106 |
107 | @CommandLine.Option(
108 | names = {"--runtime-cleanup-option"},
109 | description = "Options for the cleanup command of the selected runtime."
110 | )
111 | public List runtimeCleanupOptions;
112 |
113 | @CommandLine.Option(
114 | names = {"--volume"},
115 | description = "Mount a volume into the container."
116 | )
117 | public List volumes;
118 |
119 | @CommandLine.Option(
120 | names = {"--missing-volumes"},
121 | description = "Specifies how missing local volume directories should be handles. Valid values: ${COMPLETION-CANDIDATES}",
122 | defaultValue = "CREATE"
123 | )
124 | public ShellVolumeBehavior missingVolumes;
125 |
126 | @CommandLine.Option(
127 | names = {"--env"},
128 | description = "Specify a environment variable for the container."
129 | )
130 | public List variables;
131 |
132 | @CommandLine.Option(
133 | names = {"--publish"},
134 | description = "Publish container ports to the host system."
135 | )
136 | public List ports;
137 |
138 | @CommandLine.Parameters(
139 | index = "0",
140 | description = "The OCI image to use. In case --containerfile or --dockerfile is given as well, this defines the name of the resulting image.",
141 | defaultValue = "fedora:latest"
142 | )
143 | public String image;
144 |
145 | @CommandLine.Parameters(
146 | index = "1..*",
147 | description = "Command and its option(s) to run inside the container. Overwrites the command specified in the image."
148 | )
149 | public List commands;
150 |
151 | @Override
152 | public boolean debug() {
153 | return debug;
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/src/test/java/wtf/metio/ilo/PicocliBooleanTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-FileCopyrightText: The ilo Authors
3 | * SPDX-License-Identifier: 0BSD
4 | */
5 |
6 | package wtf.metio.ilo;
7 |
8 | import org.junit.jupiter.api.Test;
9 | import picocli.CommandLine;
10 |
11 | import static org.junit.jupiter.api.Assertions.*;
12 |
13 | public class PicocliBooleanTest {
14 |
15 | @CommandLine.Command
16 | static class TestCommand implements Runnable {
17 |
18 | @CommandLine.Option(
19 | names = {"--no-interactive"},
20 | defaultValue = "true",
21 | negatable = true
22 | )
23 | public boolean interactive;
24 |
25 | @CommandLine.Option(
26 | names = {"--no-autonomous"},
27 | defaultValue = "false",
28 | negatable = true
29 | )
30 | public boolean autonomous;
31 |
32 | @CommandLine.Option(
33 | names = {"--daemon"},
34 | defaultValue = "true",
35 | negatable = true
36 | )
37 | public boolean daemon;
38 |
39 | @CommandLine.Option(
40 | names = {"--human"},
41 | defaultValue = "false",
42 | negatable = true
43 | )
44 | public boolean human;
45 |
46 | @CommandLine.Option(
47 | names = {"--wanted"},
48 | defaultValue = "true",
49 | fallbackValue = "true",
50 | negatable = true
51 | )
52 | public boolean wanted;
53 |
54 | @Override
55 | public void run() {
56 | }
57 |
58 | }
59 |
60 | @Test
61 | public void optionNotSpecified() {
62 | final var command = new TestCommand();
63 | new CommandLine(command).parseArgs();
64 | assertAll(
65 | () -> assertTrue(command.interactive, "interactive"),
66 | () -> assertFalse(command.autonomous, "autonomous"),
67 | () -> assertTrue(command.daemon, "daemon"),
68 | () -> assertFalse(command.human, "human"),
69 | () -> assertTrue(command.wanted, "wanted")
70 | );
71 | }
72 |
73 | @Test
74 | public void optionSpecifiedWithoutValue() {
75 | final var command = new TestCommand();
76 | new CommandLine(command).parseArgs("--interactive", "--autonomous", "--daemon", "--human", "--wanted");
77 | assertAll(
78 | () -> assertTrue(command.interactive, "interactive"),
79 | () -> assertFalse(command.autonomous, "autonomous"),
80 | () -> assertFalse(command.daemon, "daemon"),
81 | () -> assertTrue(command.human, "human"),
82 | () -> assertTrue(command.wanted, "wanted")
83 | );
84 | }
85 |
86 | @Test
87 | public void optionSpecifiedWithBooleanTrue() {
88 | final var command = new TestCommand();
89 | new CommandLine(command).parseArgs("--interactive=true", "--autonomous=true", "--daemon=true", "--human=true", "--wanted=true");
90 | assertAll(
91 | () -> assertFalse(command.interactive, "interactive"),
92 | () -> assertFalse(command.autonomous, "autonomous"),
93 | () -> assertTrue(command.daemon, "daemon"),
94 | () -> assertTrue(command.human, "human"),
95 | () -> assertTrue(command.wanted, "wanted")
96 | );
97 | }
98 |
99 | @Test
100 | public void optionSpecifiedWithBooleanFalse() {
101 | final var command = new TestCommand();
102 | new CommandLine(command).parseArgs("--interactive=false", "--autonomous=false", "--daemon=false", "--human=false", "--wanted=false");
103 | assertAll(
104 | () -> assertTrue(command.interactive, "interactive"),
105 | () -> assertTrue(command.autonomous, "autonomous"),
106 | () -> assertFalse(command.daemon, "daemon"),
107 | () -> assertFalse(command.human, "human"),
108 | () -> assertFalse(command.wanted, "wanted")
109 | );
110 | }
111 |
112 | @Test
113 | public void optionSpecifiedWithNegatedForm() {
114 | final var command = new TestCommand();
115 | new CommandLine(command).parseArgs("--no-interactive", "--no-autonomous", "--no-daemon", "--no-human", "--no-wanted");
116 | assertAll(
117 | () -> assertFalse(command.interactive, "interactive"),
118 | () -> assertTrue(command.autonomous, "autonomous"),
119 | () -> assertTrue(command.daemon, "daemon"),
120 | () -> assertFalse(command.human, "human"),
121 | () -> assertFalse(command.wanted, "wanted")
122 | );
123 | }
124 |
125 | @Test
126 | public void optionSpecifiedWithNegatedFormUsingBooleanTrue() {
127 | final var command = new TestCommand();
128 | new CommandLine(command).parseArgs("--no-interactive=true", "--no-autonomous=true", "--no-daemon=true", "--no-human=true", "--no-wanted=true");
129 | assertAll(
130 | () -> assertTrue(command.interactive, "interactive"),
131 | () -> assertTrue(command.autonomous, "autonomous"),
132 | () -> assertFalse(command.daemon, "daemon"),
133 | () -> assertFalse(command.human, "human"),
134 | () -> assertFalse(command.wanted, "wanted")
135 | );
136 | }
137 |
138 | @Test
139 | public void optionSpecifiedWithNegatedFormUsingBooleanFalse() {
140 | final var command = new TestCommand();
141 | new CommandLine(command).parseArgs("--no-interactive=false", "--no-autonomous=false", "--no-daemon=false", "--no-human=false", "--no-wanted=false");
142 | assertAll(
143 | () -> assertFalse(command.interactive, "interactive"),
144 | () -> assertFalse(command.autonomous, "autonomous"),
145 | () -> assertTrue(command.daemon, "daemon"),
146 | () -> assertTrue(command.human, "human"),
147 | () -> assertTrue(command.wanted, "wanted")
148 | );
149 | }
150 |
151 | }
152 |
--------------------------------------------------------------------------------