##// END OF EJS Templates
Reworked shell execution traits, removed useless wrappers, added JsonDelegate for using with closures to construct json-like objects.
cin -
r18:97a649d829ac default
parent child
Show More
@@ -0,0 +1,15
1 package org.implab.gradle.common.dsl;
2
3 import org.implab.gradle.common.exec.Shell;
4
5 public interface ShellSpecMixin {
6 void setShell(Shell shell);
7
8 default void useEchoShell() {
9 setShell(Shell.echo());
10 }
11
12 default void useSystemShell() {
13 setShell(Shell.system());
14 }
15 }
@@ -0,0 +1,212
1 package org.implab.gradle.common.exec;
2
3 import java.util.HashMap;
4 import java.util.Map;
5 import java.util.Optional;
6 import java.util.concurrent.CompletableFuture;
7
8 import org.eclipse.jdt.annotation.NonNullByDefault;
9 import org.eclipse.jdt.annotation.Nullable;
10
11 import java.io.File;
12 import java.io.IOException;
13
14 import static java.util.Objects.requireNonNull;
15
16 /** Command line builder */
17 @NonNullByDefault
18 public abstract class AbstractExecBuilder<CS> implements ShellExec {
19
20 private boolean inheritEnvironment = true;
21
22 private final Map<String, String> environment = new HashMap<>();
23
24 private @Nullable File directory;
25
26 private RedirectFrom inputRedirect;
27
28 private RedirectTo outputRedirect;
29
30 private RedirectTo errorRedirect;
31
32 private final CS command;
33
34 protected AbstractExecBuilder(CS command) {
35 this.command = command;
36 }
37
38
39 /** Sets the working directory */
40 @Override
41 public ShellExec workingDirectory(File directory) {
42 requireNonNull(directory, "directory parameter can't be null");
43
44 this.directory = directory;
45 return this;
46 }
47
48 @Override
49 public ShellExec workingDirectory(Optional<? extends File> directory) {
50 requireNonNull(directory, "directory parameter can't be null");
51
52 this.directory = directory.orElse(null);
53 return this;
54 }
55
56 @Override
57 public ShellExec inheritEnvironment(boolean inherit) {
58 this.inheritEnvironment = inherit;
59 return this;
60 }
61
62 /**
63 * Sets the environment value. The value cannot be null.
64 *
65 * @param envVar The name of the environment variable
66 * @param value The value to set,
67 */
68 @Override
69 public ShellExec putEnvironment(String envVar, String value) {
70 requireNonNull(value, "Value can't be null");
71 requireNonNull(envVar, "envVar parameter can't be null");
72
73 environment.put(envVar, value);
74 return this;
75 }
76
77 @Override
78 public ShellExec environment(Map<String, ? extends String> env) {
79 requireNonNull(env, "env parameter can't be null");
80
81 environment.clear();
82 environment.putAll(env);
83 return this;
84 }
85
86 /**
87 * Sets redirection for the stdin, {@link RedirectFrom} will be applied
88 * every time the process is started.
89 *
90 * <p>
91 * If redirection
92 */
93 @Override
94 public ShellExec stdin(RedirectFrom from) {
95 requireNonNull(from, "from parameter can't be null");
96
97 inputRedirect = from;
98 return this;
99 }
100
101 @Override
102 public ShellExec stdin(Optional<? extends RedirectFrom> from) {
103 requireNonNull(from, "from parameter can't be null");
104 inputRedirect = from.orElse(null);
105 return this;
106 }
107
108 /**
109 * Sets redirection for the stdout, {@link RedirectTo} will be applied
110 * every time the process is started.
111 */
112 @Override
113 public ShellExec stdout(RedirectTo out) {
114 requireNonNull(out, "out parameter can't be null");
115 outputRedirect = out;
116 return this;
117 }
118
119 @Override
120 public ShellExec stdout(Optional<? extends RedirectTo> to) {
121 requireNonNull(to, "from parameter can't be null");
122 outputRedirect = to.orElse(null);
123 return this;
124 }
125
126 /**
127 * Sets redirection for the stderr, {@link RedirectTo} will be applied
128 * every time the process is started.
129 */
130 @Override
131 public ShellExec stderr(RedirectTo err) {
132 requireNonNull(err, "err parameter can't be null");
133 errorRedirect = err;
134 return this;
135 }
136
137 @Override
138 public ShellExec stderr(Optional<? extends RedirectTo> to) {
139 requireNonNull(to, "from parameter can't be null");
140 errorRedirect = to.orElse(null);
141 return this;
142 }
143
144 @Override
145 public ShellExec from(PipeSpec pipeSpec) {
146 ShellExec.super.from(pipeSpec);
147 return this;
148 }
149
150 @Override
151 public ShellExec from(EnvironmentSpec environmentSpec) {
152 ShellExec.super.from(environmentSpec);
153 return this;
154 }
155
156 /** Implement this function to */
157 protected abstract CompletableFuture<Integer> startInternal(
158 CS command,
159 EnvironmentSpec environment,
160 PipeSpec redirect) throws IOException;
161
162 /**
163 * Creates and starts new process and returns {@link CompletableFuture}. The
164 * process may be a remote process, or a shell process or etc.
165 *
166 * @return
167 * @throws IOException
168 */
169 public CompletableFuture<Integer> exec() throws IOException {
170 return startInternal(
171 command,
172 new SelfEnvironmentSpec(),
173 new SelfPipeSpec());
174 }
175
176 private class SelfEnvironmentSpec implements EnvironmentSpec {
177 @Override
178 public boolean inheritEnvironment() {
179 return inheritEnvironment;
180 }
181
182 @Override
183 public Map<String, String> environment() {
184 return environment;
185 }
186
187 @Override
188 public Optional<File> workingDirectory() {
189 return Optional.ofNullable(directory);
190 }
191 }
192
193 private class SelfPipeSpec implements PipeSpec {
194
195 @Override
196 public Optional<RedirectTo> stdout() {
197 return Optional.ofNullable(outputRedirect);
198 }
199
200 @Override
201 public Optional<RedirectTo> stderr() {
202 return Optional.ofNullable(errorRedirect);
203 }
204
205 @Override
206 public Optional<RedirectFrom> stdin() {
207 return Optional.ofNullable(inputRedirect);
208 }
209
210 }
211
212 }
@@ -0,0 +1,47
1 package org.implab.gradle.common.exec;
2
3 import java.util.List;
4 import static java.util.Objects.requireNonNull;
5
6 import java.util.ArrayList;
7
8 public record CommandSpecRecord(String executable, List<String> arguments) implements CommandSpec {
9
10 static class Builder implements CommandBuilder {
11
12 private String executable;
13
14 private List<String> arguments = new ArrayList<>();
15
16 @Override
17 public CommandBuilder executable(String executable) {
18 requireNonNull(executable, "cmd can't be null");
19 this.executable = executable;
20 return this;
21 }
22
23 @Override
24 public CommandBuilder arguments(Iterable<String> args) {
25 requireNonNull(args, "Args must not be null");
26 arguments.clear();
27 for (var arg : args)
28 arguments.add(arg);
29 return this;
30 }
31
32 @Override
33 public CommandBuilder addArguments(String... args) {
34 for (var arg : args)
35 arguments.add(requireNonNull(arg, "arguments element shouldn't be null"));
36
37 return this;
38 }
39
40 @Override
41 public CommandSpec build() {
42 requireNonNull(executable, "Executable must be specified");
43 return new CommandSpecRecord(executable, arguments);
44 }
45
46 }
47 }
@@ -0,0 +1,22
1 package org.implab.gradle.common.exec;
2
3 public interface Shell {
4
5 ShellExec of(CommandSpec spec);
6
7 /** Creates a new shell to start processes using ProcessBuilder. */
8 static Shell system() {
9 return spec -> new SystemExecBuilder(spec);
10 }
11
12 /** Creates a stub shell which echoes the specified command line. */
13 static Shell echo() {
14 return spec -> new EchoExecBuilder(spec, false);
15 }
16
17 /** Creates a stub shell which echoes the specified command line. */
18 static Shell echoStderr() {
19 return spec -> new EchoExecBuilder(spec, false);
20 }
21
22 }
@@ -0,0 +1,36
1 package org.implab.gradle.common.exec;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.List;
6 import java.util.concurrent.CompletableFuture;
7
8 import org.implab.gradle.common.utils.Exceptions;
9
10 public interface ShellExec extends PipeBuilder, EnvironmentBuilder {
11
12 default CompletableFuture<List<String>> readAllLines() throws IOException {
13 List<String> lines = new ArrayList<>();
14 stdout(RedirectTo.consumer(lines::add));
15
16 return exec()
17 .thenAccept(Exceptions.unchecked(this::assertExitCode))
18 .thenApply(v -> lines);
19 }
20
21 default CompletableFuture<String> readAllText() throws IOException {
22 List<String> lines = new ArrayList<>();
23 stdout(RedirectTo.consumer(lines::add));
24
25 return exec()
26 .thenAccept(Exceptions.unchecked(this::assertExitCode))
27 .thenApply(v -> String.join("\n", lines));
28 }
29
30 default void assertExitCode(Integer code) throws IOException {
31 if (code != 0)
32 throw new IOException(String.format("The process is terminated with code %d", code));
33 }
34
35 CompletableFuture<Integer> exec() throws IOException;
36 }
@@ -0,0 +1,108
1 package org.implab.gradle.common.tasks;
2
3 import java.io.IOException;
4 import java.util.Map;
5 import java.util.Optional;
6 import java.util.concurrent.ExecutionException;
7
8 import org.gradle.api.DefaultTask;
9 import org.gradle.api.file.DirectoryProperty;
10 import org.gradle.api.provider.MapProperty;
11 import org.gradle.api.provider.Property;
12 import org.gradle.api.tasks.Internal;
13 import org.gradle.api.tasks.TaskAction;
14 import org.implab.gradle.common.dsl.RedirectFromSpec;
15 import org.implab.gradle.common.dsl.RedirectToSpec;
16 import org.implab.gradle.common.dsl.TaskEnvSpecMixin;
17 import org.implab.gradle.common.dsl.TaskPipeSpecMixin;
18 import org.implab.gradle.common.exec.RedirectTo;
19 import org.implab.gradle.common.exec.ShellExec;
20 import org.implab.gradle.common.utils.Exceptions;
21 import org.implab.gradle.common.utils.Values;
22
23 public abstract class AbstractShellExecTask
24 extends DefaultTask
25 implements TaskPipeSpecMixin, TaskEnvSpecMixin {
26
27 private final RedirectToSpec redirectStderr = new RedirectToSpec();
28
29 private final RedirectToSpec redirectStdout = new RedirectToSpec();
30
31 private final RedirectFromSpec redirectStdin = new RedirectFromSpec();
32
33 @Internal
34 @Override
35 public abstract Property<Boolean> getInheritEnvironment();
36
37 @Internal
38 @Override
39 public abstract DirectoryProperty getWorkingDirectory();
40
41 @Internal
42 @Override
43 public abstract MapProperty<String, String> getEnvironment();
44
45 /**
46 * STDIN redirection, if not specified, no input will be passed to the command
47 */
48 @Internal
49 @Override
50 public RedirectFromSpec getStdin() {
51 return redirectStdin;
52 }
53
54 /**
55 * STDOUT redirection, if not specified, redirected to logger::info
56 */
57 @Internal
58 @Override
59 public RedirectToSpec getStdout() {
60 return redirectStdout;
61 }
62
63 /**
64 * STDERR redirection, if not specified, redirected to logger::error
65 */
66 @Internal
67 @Override
68 public RedirectToSpec getStderr() {
69 return redirectStderr;
70 }
71
72 protected abstract ShellExec execBuilder();
73
74 protected Optional<RedirectTo> conventionalStderr() {
75 return Optional.of(RedirectTo.eachLine(getLogger()::error));
76 }
77
78 protected Optional<RedirectTo> conventionalStdout() {
79 return Optional.of(RedirectTo.eachLine(getLogger()::info));
80 }
81
82 @TaskAction
83 public final void run() throws IOException, InterruptedException, ExecutionException {
84 // create new shell process
85 var execBuilder = execBuilder();
86
87 // configure environment
88 execBuilder
89 .workingDirectory(Values.optional(getWorkingDirectory().getAsFile()))
90 .environment(getEnvironment().getOrElse(Map.of()));
91
92 // configure redirects
93 execBuilder
94 .stdout(getStdout().getRedirection().or(this::conventionalStdout))
95 .stderr(getStderr().getRedirection().or(this::conventionalStderr))
96 .stdin(getStdin().getRedirection());
97
98 // execute
99 execBuilder.exec()
100 .thenAccept(Exceptions.unchecked(this::assertExitCode))
101 .join();
102 }
103
104 protected void assertExitCode(Integer code) throws IOException {
105 if (code != 0)
106 throw new IOException(String.format("The process is terminated with code %s", code));
107 }
108 } No newline at end of file
@@ -0,0 +1,51
1 package org.implab.gradle.common.utils;
2
3 import java.util.function.Consumer;
4 import java.util.function.Function;
5
6 public class Exceptions {
7 /**
8 * Helper function which declares that this block can throw the specified
9 * exception.
10 *
11 * @param <E>
12 * @param clazz
13 * @throws E
14 */
15 @SuppressWarnings("unused")
16 public static <E extends Throwable> void mayThrow(Class<E> clazz) throws E {
17 }
18
19 @SuppressWarnings("unchecked")
20 public static <E extends Throwable> E sneakyThrow(Throwable t) throws E {
21 throw (E) t;
22 }
23
24 public static <T, U> Function<T, U> unchecked(ThrowingFunction<T, U> fn) {
25 return val -> {
26 try {
27 return fn.apply(val);
28 } catch (Exception e) {
29 throw sneakyThrow(e);
30 }
31 };
32 }
33
34 public static <T> Consumer<T> unchecked(ThrowingConsumer<T> c) {
35 return val -> {
36 try {
37 c.accept(val);
38 } catch (Exception e) {
39 throw sneakyThrow(e);
40 }
41 };
42 }
43
44 public interface ThrowingConsumer<T> {
45 void accept(T value) throws Exception;
46 }
47
48 public interface ThrowingFunction<T, U> {
49 U apply(T value) throws Exception;
50 }
51 }
@@ -0,0 +1,84
1 package org.implab.gradle.common.utils;
2
3 import java.util.LinkedHashMap;
4 import java.util.Map;
5 import java.util.Objects;
6 import java.util.function.Function;
7 import java.util.stream.Stream;
8
9 import groovy.lang.Closure;
10 import groovy.lang.GroovyObjectSupport;
11
12 public class JsonDelegate extends GroovyObjectSupport {
13 private final Map<String, Object> content;
14
15 private JsonDelegate() {
16 content = new LinkedHashMap<>();
17 }
18
19 private JsonDelegate(Map<String, Object> content) {
20 this.content = new LinkedHashMap<>(content);
21 }
22
23 public Object invokeMethod(String name, Object args) {
24 Object val = null;
25 if (args != null && Object[].class.isAssignableFrom(args.getClass())) {
26 Object[] arr = (Object[]) args;
27 if (arr.length == 1) {
28 val = processValue(arr[0], content.get(name));
29 } else if (arr.length == 2 && arr[1] instanceof Closure<?> c) {
30 if (arr[0] instanceof Iterable<?> iter) {
31 val = processStream(Values.stream(iter.iterator()), c);
32 } else if(arr[0] != null && arr[0].getClass().isArray()) {
33 val = processStream(Stream.of((Object)arr[0]), c);
34 } else {
35 val = arr;
36 }
37 } else {
38 val = arr;
39 }
40 }
41
42 this.content.put(name, val);
43 return val;
44 }
45
46 private Object processValue(Object value, Object original) {
47 if (value instanceof Closure<?> c) {
48 return Objects.nonNull(original) ? of(c, original) : of(c);
49 } else {
50 return value;
51 }
52 }
53
54 private Object processStream(Stream<?> stream, Closure<?> c) {
55 return stream.map(transform(c)).toArray();
56 }
57
58 public static Map<String, Object> of(Closure<?> c) {
59 return invoke((Closure<?>) c.clone(), new JsonDelegate());
60 }
61
62 public static Function<Object, Map<String, Object>> transform(Closure<?> c) {
63 return o -> of(c, o);
64 }
65
66 public static Map<String, Object> of(Closure<?> c, Object o) {
67 return invoke((Closure<?>) c.curry(o), new JsonDelegate());
68 }
69
70 public static Map<String, Object> with(Closure<?> c, Map<String, Object> m) {
71 return invoke((Closure<?>) c.clone(), new JsonDelegate(m));
72 }
73
74 private static Map<String, Object> invoke(Closure<?> c, JsonDelegate d) {
75 c.setDelegate(d);
76 c.setResolveStrategy(1);
77 c.call();
78 return d.getContent();
79 }
80
81 public Map<String, Object> getContent() {
82 return this.content;
83 }
84 }
@@ -0,0 +1,63
1 package org.implab.gradle.common.utils;
2
3 import java.util.HashMap;
4 import java.util.Map;
5 import java.util.function.Function;
6
7 import org.gradle.api.Action;
8 import org.gradle.api.provider.ListProperty;
9 import org.gradle.api.provider.MapProperty;
10 import org.gradle.api.provider.Provider;
11
12 public final class Properties {
13 private Properties() {
14 }
15
16 public static <K> void mergeMap(MapProperty<K, Object> property, Map<K, Object> map) {
17 map.forEach((k, v) -> {
18 if (v instanceof Provider<?>)
19 property.put(k, (Provider<?>) v);
20 else
21 property.put(k, v);
22 });
23 }
24
25 public static <K, V> void mergeMap(MapProperty<K, V> property, Map<K, ?> map, Function<Object, V> mapper) {
26 map.forEach((k, v) -> {
27 if (v instanceof Provider<?>)
28 property.put(k, ((Provider<?>) v).map(mapper::apply));
29 else
30 property.put(k, mapper.apply(v));
31 });
32 }
33
34 public static void mergeList(ListProperty<Object> property, Iterable<Object> values) {
35 values.forEach(v -> {
36 if (v instanceof Provider<?>)
37 property.add((Provider<?>) v);
38 else
39 property.add(v);
40 });
41 }
42
43 public static <V> void mergeList(ListProperty<V> property, Iterable<Object> values, Function<Object, V> mapper) {
44 values.forEach(v -> {
45 if (v instanceof Provider<?>)
46 property.add(((Provider<?>) v).map(mapper::apply));
47 else
48 property.add(mapper.apply(v));
49 });
50 }
51
52 public static <K> void configureMap(MapProperty<K, Object> prop, Action<Map<K, ?>> configure) {
53 var map = new HashMap<K, Object>();
54 configure.execute(map);
55 mergeMap(prop, map);
56 }
57
58 public static <K, V> void configureMap(MapProperty<K, V> prop, Action<Map<K, ?>> configure, Function<Object, V> mapper) {
59 var map = new HashMap<K, Object>();
60 configure.execute(map);
61 mergeMap(prop, map, mapper);
62 }
63 }
@@ -20,26 +20,30 public class RedirectFromSpec {
20 20 }
21 21
22 22 public void fromFile(File file) {
23 this.streamRedirect = () -> RedirectFrom.file(file);
23 streamRedirect = () -> RedirectFrom.file(file);
24 24 }
25 25
26 26 public void fromFile(Provider<File> fileProvider) {
27 this.streamRedirect = fileProvider.map(RedirectFrom::file)::getOrNull;
27 streamRedirect = fileProvider.map(RedirectFrom::file)::getOrNull;
28 28 }
29 29
30 30 public void fromStream(InputStream stream) {
31 this.streamRedirect = () -> RedirectFrom.stream(stream);
31 streamRedirect = () -> RedirectFrom.stream(stream);
32 32 }
33 33
34 34 public void fromStream(Provider<InputStream> streamProvider) {
35 this.streamRedirect = streamProvider.map(RedirectFrom::stream)::getOrNull;
35 streamRedirect = streamProvider.map(RedirectFrom::stream)::getOrNull;
36 36 }
37 37
38 38 public void from(Object input) {
39 39 if (input instanceof Provider<?> inputProvider) {
40 this.streamRedirect = inputProvider.map(RedirectFrom::any)::get;
40 streamRedirect = inputProvider.map(RedirectFrom::any)::get;
41 41 } else {
42 this.streamRedirect = () -> RedirectFrom.any(input);
42 streamRedirect = () -> RedirectFrom.any(input);
43 43 }
44 44 }
45
46 public void empty() {
47 streamRedirect = () -> null;
45 48 }
49 }
@@ -28,30 +28,38 public class RedirectToSpec {
28 28 }
29 29
30 30 public void toFile(File file) {
31 this.streamRedirect = () -> RedirectTo.file(file);
31 streamRedirect = () -> RedirectTo.file(file);
32 32 }
33 33
34 34 public void toFile(Provider<File> fileProvider) {
35 this.streamRedirect = fileProvider.map(RedirectTo::file)::getOrNull;
35 streamRedirect = fileProvider.map(RedirectTo::file)::getOrNull;
36 36 }
37 37
38 38 public void toStream(OutputStream stream) {
39 this.streamRedirect = () -> RedirectTo.stream(stream);
39 streamRedirect = () -> RedirectTo.stream(stream);
40 40 }
41 41
42 42 public void toStream(Provider<OutputStream> streamProvider) {
43 this.streamRedirect = streamProvider.map(RedirectTo::stream)::getOrNull;
43 streamRedirect = streamProvider.map(RedirectTo::stream)::getOrNull;
44 44 }
45 45
46 46 public void to(Object output) {
47 47 if (output instanceof Provider<?> outputProvider) {
48 this.streamRedirect = outputProvider.map(RedirectTo::any)::get;
48 streamRedirect = outputProvider.map(RedirectTo::any)::get;
49 49 } else {
50 this.streamRedirect = () -> RedirectTo.any(output);
50 streamRedirect = () -> RedirectTo.any(output);
51 51 }
52 52 }
53 53
54 public void consume(Consumer<String> consumer) {
55 this.streamRedirect = () -> RedirectTo.consumer(consumer);
54 public void eachLine(Consumer<String> consumer) {
55 streamRedirect = () -> RedirectTo.eachLine(consumer);
56 }
57
58 public void allText(Consumer<String> consumer) {
59 streamRedirect = () -> RedirectTo.allText(consumer);
60 }
61
62 public void discard() {
63 streamRedirect = () -> null;
56 64 }
57 65 }
@@ -1,13 +1,26
1 1 package org.implab.gradle.common.dsl;
2 2
3 import java.util.stream.Stream;
3 4
4 5 import org.gradle.api.provider.ListProperty;
6 import org.implab.gradle.common.utils.Properties;
5 7
6 8 public interface TaskCommandSpecMixin {
7 9 ListProperty<String> getCommandLine();
8 10
9 void commandLine(Object arg0, Object... args);
11 default void commandLine(Object arg0, Object... args) {
12 getCommandLine().empty();
13 Properties.mergeList(
14 getCommandLine(),
15 () -> Stream.concat(Stream.of(arg0), Stream.of(args)).iterator(),
16 Object::toString);
17 }
10 18
11 void args(Object arg0, Object... args);
19 default void args(Object arg0, Object... args) {
20 Properties.mergeList(
21 getCommandLine(),
22 () -> Stream.concat(Stream.of(arg0), Stream.of(args)).iterator(),
23 Object::toString);
24 }
12 25
13 26 }
@@ -1,15 +1,13
1 1 package org.implab.gradle.common.dsl;
2 2
3 import java.util.HashMap;
4 3 import java.util.Map;
5 4
6 5 import org.gradle.api.Action;
7 6 import org.gradle.api.file.DirectoryProperty;
8 7 import org.gradle.api.provider.MapProperty;
9 8 import org.gradle.api.provider.Property;
10 import org.gradle.api.provider.Provider;
11 9 import org.implab.gradle.common.utils.Closures;
12 import org.implab.gradle.common.utils.Values;
10 import org.implab.gradle.common.utils.Properties;
13 11
14 12 import groovy.lang.Closure;
15 13
@@ -38,7 +36,7 public interface TaskEnvSpecMixin {
38 36 * properties may be assigned to providers.
39 37 */
40 38 default void env(Action<Map<String, ?>> configure) {
41 Values.configureMap(getEnvironment(), configure, Object.class, Values::toString);
39 Properties.configureMap(getEnvironment(), configure, Object::toString);
42 40 }
43 41
44 42 default void env(Closure<?> configure) {
@@ -80,4 +80,6 public interface CommandBuilder {
80 80 return executable(commandSpec.executable())
81 81 .arguments(commandSpec.arguments());
82 82 }
83
84 CommandSpec build();
83 85 }
@@ -13,4 +13,8 public interface CommandSpec {
13 13 return Stream.concat(Stream.of(executable()), arguments().stream()).toList();
14 14 }
15 15
16 public static CommandBuilder builder() {
17 return new CommandSpecRecord.Builder();
16 18 }
19
20 }
@@ -6,11 +6,12 import java.nio.charset.StandardCharsets
6 6 import java.util.concurrent.CompletableFuture;
7 7 import java.util.stream.Collectors;
8 8
9 class EchoExecBuilder extends ExecBuilder {
9 class EchoExecBuilder extends AbstractExecBuilder<CommandSpec> {
10 10
11 11 private final boolean echoToStderr;
12 12
13 public EchoExecBuilder(boolean echoToStderr) {
13 public EchoExecBuilder(CommandSpec command, boolean echoToStderr) {
14 super(command);
14 15 this.echoToStderr = echoToStderr;
15 16 }
16 17
@@ -4,6 +4,12 import java.util.Optional;
4 4
5 5 import org.eclipse.jdt.annotation.NonNullByDefault;
6 6
7 /**
8 * The execution shell uses this specification when starting
9 * a new process. The shell may check for the specified
10 * redirections and apply them when launching the process.
11 * The exact moment they are applied is at the shell’s discretion.
12 */
7 13 @NonNullByDefault
8 14 public interface PipeSpec {
9 15 Optional<RedirectTo> stdout();
@@ -7,6 +7,8 import java.io.OutputStream;
7 7 import java.util.Scanner;
8 8 import java.util.concurrent.CompletableFuture;
9 9 import java.util.function.Consumer;
10 import java.util.stream.Collectors;
11 import java.util.stream.Stream;
10 12
11 13 import org.eclipse.jdt.annotation.NonNullByDefault;
12 14
@@ -21,10 +23,12 public interface RedirectTo {
21 23 CompletableFuture<Void> redirect(InputStream from);
22 24
23 25 public interface StringConsumer extends Consumer<String> {
26 default void complete() {
27 }
24 28 }
25 29
26 30 /** Creates a redirect to the specified consumer */
27 public static RedirectTo consumer(final Consumer<String> consumer) {
31 public static RedirectTo eachLine(final Consumer<String> consumer) {
28 32 return consumer(new StringConsumer() {
29 33 @Override
30 34 public void accept(String s) {
@@ -33,6 +37,22 public interface RedirectTo {
33 37 });
34 38 }
35 39
40 public static RedirectTo allText(final Consumer<String> consumer) {
41 return consumer(new StringConsumer() {
42 final Stream.Builder<String> builder = Stream.builder();
43
44 @Override
45 public void accept(String s) {
46 builder.accept(s);
47 }
48
49 @Override
50 public void complete() {
51 consumer.accept(builder.build().collect(Collectors.joining("\n")));
52 }
53 });
54 }
55
36 56 /** Creates a redirect to the specified consumer */
37 57 public static RedirectTo consumer(final StringConsumer consumer) {
38 58 return (src) -> CompletableFuture.runAsync(() -> {
@@ -40,6 +60,7 public interface RedirectTo {
40 60 while (sc.hasNextLine()) {
41 61 consumer.accept(sc.nextLine());
42 62 }
63 consumer.complete();
43 64 }
44 65 });
45 66 }
@@ -51,7 +72,7 public interface RedirectTo {
51 72 */
52 73 public static RedirectTo file(final File file) {
53 74 return src -> CompletableFuture.runAsync(() -> {
54 try (OutputStream out = new FileOutputStream(file)) {
75 try (src; OutputStream out = new FileOutputStream(file)) {
55 76 src.transferTo(out);
56 77 } catch (Exception e) {
57 78 // silence!
@@ -5,7 +5,11 import java.lang.ProcessBuilder.Redirect
5 5 import java.util.ArrayList;
6 6 import java.util.concurrent.CompletableFuture;
7 7
8 class SystemExecBuilder extends ExecBuilder {
8 class SystemExecBuilder extends AbstractExecBuilder<CommandSpec> {
9 SystemExecBuilder(CommandSpec command) {
10 super(command);
11 }
12
9 13 @Override
10 14 protected CompletableFuture<Integer> startInternal(
11 15 CommandSpec command,
@@ -1,115 +1,31
1 1 package org.implab.gradle.common.tasks;
2 2
3 import java.io.IOException;
4 import java.util.Map;
5 import java.util.concurrent.ExecutionException;
6 import java.util.stream.Stream;
7
8 import org.gradle.api.DefaultTask;
9 import org.gradle.api.file.DirectoryProperty;
10 3 import org.gradle.api.provider.ListProperty;
11 import org.gradle.api.provider.MapProperty;
12 4 import org.gradle.api.provider.Property;
13 5 import org.gradle.api.tasks.Internal;
14 import org.gradle.api.tasks.TaskAction;
15 6 import org.implab.gradle.common.dsl.TaskCommandSpecMixin;
16 import org.implab.gradle.common.dsl.TaskPipeSpecMixin;
17 import org.implab.gradle.common.dsl.RedirectFromSpec;
18 import org.implab.gradle.common.dsl.RedirectToSpec;
19 import org.implab.gradle.common.dsl.TaskEnvSpecMixin;
20 import org.implab.gradle.common.exec.ExecBuilder;
21 import org.implab.gradle.common.utils.ObjectsMixin;
22 import org.implab.gradle.common.utils.Strings;
23 import org.implab.gradle.common.utils.ThrowingConsumer;
7 import org.implab.gradle.common.exec.CommandSpec;
8 import org.implab.gradle.common.exec.Shell;
9 import org.implab.gradle.common.exec.ShellExec;
24 10
25 11 public abstract class ShellExecTask
26 extends DefaultTask
27 implements TaskCommandSpecMixin, TaskPipeSpecMixin, TaskEnvSpecMixin, ObjectsMixin {
28
29 private final RedirectToSpec redirectStderr = new RedirectToSpec();
30
31 private final RedirectToSpec redirectStdout = new RedirectToSpec();
32
33 private final RedirectFromSpec redirectStdin = new RedirectFromSpec();
12 extends AbstractShellExecTask
13 implements TaskCommandSpecMixin {
34 14
35 15 @Internal
36 @Override
37 public abstract Property<Boolean> getInheritEnvironment();
38
39 @Internal
40 @Override
41 public abstract DirectoryProperty getWorkingDirectory();
42
43 @Internal
44 @Override
45 public abstract MapProperty<String, String> getEnvironment();
16 public abstract Property<Shell> getShell();
46 17
47 18 @Internal
48 19 @Override
49 20 public abstract ListProperty<String> getCommandLine();
50 21
51 /**
52 * STDIN redirection, if not specified, no input will be passed to the command
53 */
54 @Internal
55 @Override
56 public RedirectFromSpec getStdin() {
57 return redirectStdin;
58 }
59
60 /**
61 * STDOUT redirection, if not specified, redirected to logger::info
62 */
63 @Internal
64 @Override
65 public RedirectToSpec getStdout() {
66 return redirectStdout;
67 }
68
69 /**
70 * STDERR redirection, if not specified, redirected to logger::error
71 */
72 @Internal
73 @Override
74 public RedirectToSpec getStderr() {
75 return redirectStderr;
76 }
77 22
78 23 @Override
79 public void commandLine(Object arg0, Object... args) {
80 getCommandLine().set(provider(() -> Stream.concat(
81 Stream.of(arg0),
82 Stream.of(args))
83 .map(Strings::asString).toList()));
84
85 }
86
87 @Override
88 public void args(Object arg0, Object... args) {
89 getCommandLine().addAll(provider(() -> Stream.concat(
90 Stream.of(arg0),
91 Stream.of(args))
92 .map(Strings::asString).toList()));
93 }
94
95 protected abstract ExecBuilder execBuilder();
96
97 @TaskAction
98 public final void run() throws IOException, InterruptedException, ExecutionException {
99 var execBuilder = execBuilder();
100 execBuilder.workingDirectory(getWorkingDirectory().get().getAsFile());
101 execBuilder.environment(getEnvironment().getOrElse(Map.of()));
102 execBuilder.commandLine(getCommandLine().get());
103
104 getStdout().getRedirection().ifPresent(execBuilder::stdout);
105 getStderr().getRedirection().ifPresent(execBuilder::stderr);
106 getStdin().getRedirection().ifPresent(execBuilder::stdin);
107
108 execBuilder.exec().thenAccept(ThrowingConsumer.guard(this::checkRetCode)).join();
109 }
110
111 protected void checkRetCode(Integer code) throws IOException {
112 throw new IOException(String.format("The process is terminated with code %s", code));
24 protected ShellExec execBuilder() {
25 return getShell().get()
26 .of(CommandSpec.builder()
27 .commandLine(getCommandLine().get())
28 .build());
113 29 }
114 30
115 31 }
@@ -23,12 +23,10 public final class Closures {
23 23 }
24 24
25 25 public static void apply(Closure<?> action, Object target) {
26 var prevDelegate = action.getDelegate();
27 try {
28 action.setDelegate(target);
29 action.call(target);
30 } finally {
31 action.setDelegate(prevDelegate);
26 var c = (Closure<?>)action.clone();
27 c.setResolveStrategy(0);
28 c.setDelegate(target);
29 c.call(target);
30
32 31 }
33 32 }
34 }
@@ -16,6 +16,28 public class Strings {
16 16 : firstLetter.matcher(string).replaceFirst(m -> m.group().toUpperCase());
17 17 }
18 18
19 public static String toCamelCase(String name) {
20 if (name == null || name.isEmpty())
21 return name;
22 StringBuilder out = new StringBuilder(name.length());
23 boolean up = false;
24 boolean first = true;
25 for (int i = 0; i < name.length(); i++) {
26 char c = name.charAt(i);
27 switch (c) {
28 case '-', '_', ' ', '.' -> up = true;
29 default -> {
30 out.append(
31 first ? Character.toLowerCase(c)
32 : up ? Character.toUpperCase(c): c);
33 up = false;
34 first = false;
35 }
36 }
37 }
38 return out.toString();
39 }
40
19 41 public static void argumentNotNullOrEmpty(String value, String argumentName) {
20 42 if (value == null || value.length() == 0)
21 43 throw new IllegalArgumentException(String.format("Argument %s can't be null or empty", argumentName));
@@ -1,19 +1,12
1 1 package org.implab.gradle.common.utils;
2 2
3 import java.util.HashMap;
4 3 import java.util.Iterator;
5 import java.util.Map;
6 4 import java.util.Spliterators;
7 import java.util.Map.Entry;
8 5 import java.util.Optional;
9 import java.util.function.Function;
10 6 import java.util.function.Supplier;
11 import java.util.stream.Collectors;
12 7 import java.util.stream.Stream;
13 8 import java.util.stream.StreamSupport;
14 9
15 import org.gradle.api.Action;
16 import org.gradle.api.provider.MapProperty;
17 10 import org.gradle.api.provider.Provider;
18 11
19 12 public final class Values {
@@ -22,50 +15,6 public final class Values {
22 15 }
23 16
24 17 /**
25 * Converts values in the specified map
26 *
27 * @param <K>
28 * @param <V>
29 * @param <U>
30 * @param map
31 * @param mapper
32 * @return
33 */
34 public static <K, V, U> Map<K, U> mapValues(Map<K, V> map, Function<V, U> mapper) {
35 Function<Entry<K, V>, V> getter = Entry::getValue;
36
37 return map.entrySet().stream()
38 .collect(Collectors.toMap(Entry::getKey, getter.andThen(mapper)));
39 }
40
41 public static <K, V> void mergeMap(MapProperty<K,V> prop, Map<K, ? > map, Class<V> clazz) {
42 map.forEach((k,v) -> {
43 if(v instanceof Provider<?>)
44 prop.put(k, ((Provider<?>)v).map(clazz::cast));
45 else
46 prop.put(k, clazz.cast(v));
47 });
48 }
49
50 public static <K,V,U> void mergeMap(MapProperty<K,V> prop, Map<K, ?> map, Class<U> clazz, Function<U,V> mapper) {
51 Function<Object, U> caster = clazz::cast;
52 var castMap = caster.andThen(mapper);
53
54 map.forEach((k,v) -> {
55 if(v instanceof Provider<?>)
56 prop.put(k, ((Provider<?>)v).map(x -> castMap.apply(x)));
57 else
58 prop.put(k, castMap.apply(v));
59 });
60 }
61
62 public static <K,V,U> void configureMap(MapProperty<K,V> prop, Action<Map<K, ?>> configure, Class<U> clazz, Function<U,V> mapper) {
63 var map = new HashMap<K,V>();
64 configure.execute(map);
65 mergeMap(prop, map, clazz, mapper);
66 }
67
68 /**
69 18 * Converts the supplied value to a string.
70 19 */
71 20 public static String toString(Object value) {
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now