##// END OF EJS Templates
Refactoring, removed GenericSourceSetOutput.
cin -
r23:8e57f0a2d4de default
parent child
Show More
@@ -1,7 +1,8
1 1 {
2 2 "java.configuration.updateBuildConfiguration": "automatic",
3 3 "java.compile.nullAnalysis.mode": "automatic",
4 4 "cSpell.words": [
5 "implab"
5 "implab",
6 "rawtypes"
6 7 ]
7 8 } No newline at end of file
@@ -1,15 +1,15
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.gradle;
2 2
3 3 import java.util.function.Consumer;
4 4
5 5 import org.gradle.api.plugins.ExtensionContainer;
6 6
7 7 public final class Extensions {
8 8 private Extensions() {
9 9 }
10 10
11 11 public static Consumer<Class<?>> registerClass(ExtensionContainer extensions) {
12 12 var extra = extensions.getExtraProperties();
13 13 return clazz -> extra.set(clazz.getSimpleName(), clazz);
14 14 }
15 15 }
@@ -1,69 +1,69
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.gradle;
2 2
3 3 import java.util.HashMap;
4 4 import java.util.Map;
5 5 import java.util.function.Function;
6 6 import org.gradle.api.Action;
7 7 import org.gradle.api.provider.ListProperty;
8 8 import org.gradle.api.provider.MapProperty;
9 9 import org.gradle.api.provider.Provider;
10 10
11 11 public final class Properties {
12 12 private Properties() {
13 13 }
14 14
15 15 public static <K> void mergeMap(MapProperty<K, Object> property, Map<K, Object> map) {
16 16 mergeMap(property, map, Function.identity());
17 17 }
18 18
19 19 public static <K, V> void putMapEntry(MapProperty<K, V> property, K key, Object value,
20 20 Function<Object, ? extends V> mapper) {
21 21 if (value instanceof Provider<?>)
22 22 property.put(key, ((Provider<?>) value).map(mapper::apply));
23 23 else
24 24 property.put(key, mapper.apply(value));
25 25 }
26 26
27 27 public static <K> void putMapEntry(MapProperty<K, Object> property, K key, Object value) {
28 28 putMapEntry(property, key, value, Function.identity());
29 29 }
30 30
31 31 public static <K, V> void putMapEntry(MapProperty<K, V> property, K key, Object value, Class<V> valueType) {
32 32 putMapEntry(property, key, value, valueType::cast);
33 33 }
34 34
35 35 public static <K, V> void mergeMap(MapProperty<K, V> property, Map<K, ?> map,
36 36 Function<Object, ? extends V> mapper) {
37 37 map.forEach((k, v) -> {
38 38 if (v instanceof Provider<?>)
39 39 property.put(k, ((Provider<?>) v).map(mapper::apply));
40 40 else
41 41 property.put(k, mapper.apply(v));
42 42 });
43 43 }
44 44
45 45 public static void mergeList(ListProperty<Object> property, Iterable<Object> values) {
46 46 mergeList(property, values, Function.identity());
47 47 }
48 48
49 49 public static <V> void mergeList(ListProperty<V> property, Iterable<Object> values,
50 50 Function<Object, ? extends V> mapper) {
51 51 values.forEach(v -> {
52 52 if (v instanceof Provider<?>)
53 53 property.add(((Provider<?>) v).map(mapper::apply));
54 54 else
55 55 property.add(mapper.apply(v));
56 56 });
57 57 }
58 58
59 59 public static <K> void configureMap(MapProperty<K, Object> prop, Action<? super Map<K, Object>> configure) {
60 60 configureMap(prop, configure, Function.identity());
61 61 }
62 62
63 63 public static <K, V> void configureMap(MapProperty<K, V> prop, Action<? super Map<K, Object>> configure,
64 64 Function<Object, V> mapper) {
65 65 var map = new HashMap<K, Object>();
66 66 configure.execute(map);
67 67 mergeMap(prop, map, mapper);
68 68 }
69 69 }
@@ -1,29 +1,29
1 package org.implab.gradle.common.tasks;
1 package org.implab.gradle.common.core.gradle;
2 2
3 3 import org.gradle.api.Project;
4 4 import org.gradle.api.Task;
5 5 import org.gradle.api.logging.Logger;
6 6 import org.gradle.api.specs.Spec;
7 7 import org.gradle.api.tasks.Internal;
8 8
9 9 /** Task methods available by default, this interface is used by mixins to
10 10 * interact with their task.
11 11 */
12 12 public interface TaskExtra {
13 13 @Internal
14 14 Project getProject();
15 15
16 16 void onlyIf(Spec<? super Task> spec);
17 17
18 18 @Internal
19 19 Logger getLogger();
20 20
21 21 default void onlyIfReason(String skipReason, Spec<? super Task> spec) {
22 22 onlyIf(self -> {
23 23 var satisfied = spec.isSatisfiedBy(self);
24 24 if (!satisfied)
25 25 getLogger().info("SKIP: {}", skipReason);
26 26 return satisfied;
27 27 });
28 28 }
29 29 }
@@ -1,31 +1,31
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.lang;
2 2
3 3 import groovy.lang.Closure;
4 4
5 5 import org.eclipse.jdt.annotation.NonNullByDefault;
6 6 import org.gradle.api.Action;
7 7
8 8 @NonNullByDefault
9 9 public final class Closures {
10 10 private Closures() {
11 11 }
12 12
13 13 /**
14 14 * Wraps {@link Action} around the specified closure. The parameter
15 15 * of the action will be used as delegate in the specified closure.
16 16 *
17 17 * @param <T> The type of the action parameter
18 18 * @param closure The closure
19 19 * @return
20 20 */
21 21 public static <T> Action<T> action(Closure<?> closure) {
22 22 return arg -> apply(closure, arg);
23 23 }
24 24
25 25 public static void apply(Closure<?> action, Object target) {
26 26 var c = (Closure<?>)action.clone();
27 27 c.setResolveStrategy(Closure.DELEGATE_FIRST);
28 28 c.setDelegate(target);
29 29 c.call(target);
30 30 }
31 31 }
@@ -1,51 +1,50
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.lang;
2 2
3 3 import java.util.function.Consumer;
4 4 import java.util.function.Function;
5 5
6 6 public class Exceptions {
7 7 /**
8 8 * Helper function which declares that this block can throw the specified
9 9 * exception.
10 10 *
11 11 * @param <E>
12 12 * @param clazz
13 13 * @throws E
14 14 */
15 @SuppressWarnings("unused")
16 15 public static <E extends Throwable> void mayThrow(Class<E> clazz) throws E {
17 16 }
18 17
19 18 @SuppressWarnings("unchecked")
20 19 public static <E extends Throwable> E sneakyThrow(Throwable t) throws E {
21 20 throw (E) t;
22 21 }
23 22
24 23 public static <T, U> Function<T, U> unchecked(ThrowingFunction<T, U> fn) {
25 24 return val -> {
26 25 try {
27 26 return fn.apply(val);
28 27 } catch (Exception e) {
29 28 throw sneakyThrow(e);
30 29 }
31 30 };
32 31 }
33 32
34 33 public static <T> Consumer<T> unchecked(ThrowingConsumer<T> c) {
35 34 return val -> {
36 35 try {
37 36 c.accept(val);
38 37 } catch (Exception e) {
39 38 throw sneakyThrow(e);
40 39 }
41 40 };
42 41 }
43 42
44 43 public interface ThrowingConsumer<T> {
45 44 void accept(T value) throws Exception;
46 45 }
47 46
48 47 public interface ThrowingFunction<T, U> {
49 48 U apply(T value) throws Exception;
50 49 }
51 50 }
@@ -1,29 +1,34
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.lang;
2 2
3 import java.util.concurrent.atomic.AtomicReference;
3 import java.util.Objects;
4 4 import java.util.function.Supplier;
5 5
6 6 public class LazyValue<T> implements Supplier<T> {
7 private final AtomicReference<T> reference = new AtomicReference<>();
7 private volatile T value;
8 8
9 9 private final Supplier<T> innerSupplier;
10 10
11 11 public LazyValue(Supplier<T> supplier) {
12 12 this.innerSupplier = supplier;
13 13 }
14 14
15 15 @Override
16 16 public T get() {
17 var t = reference.get();
18 if (t != null)
19 return t;
20 return updateValue();
21 }
17 var v = value;
18 if (v != null) {
19 return v;
20 }
22 21
23 private T updateValue() {
24 var v = innerSupplier.get();
25 var t = reference.compareAndExchange(null, v);
26 return t != null ? t : v;
22 synchronized (this) {
23 v = value;
24 if (v == null) {
25 v = Objects.requireNonNull(
26 innerSupplier.get(),
27 "LazyValue supplier returned null");
28 value = v;
29 }
30 return v;
31 }
27 32 }
28 33
29 34 }
@@ -1,316 +1,316
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.lang;
2 2
3 3 import com.fasterxml.jackson.annotation.JsonCreator;
4 4 import com.fasterxml.jackson.annotation.JsonValue;
5 5
6 6 import java.util.Objects;
7 7 import java.util.Optional;
8 8 import java.util.regex.Matcher;
9 9 import java.util.regex.Pattern;
10 10
11 11 /**
12 12 * Immutable Semantic Version (SemVer 2.0.0) of the form:
13 13 *
14 14 * <pre>
15 15 * MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
16 16 * </pre>
17 17 *
18 18 * Ordering (as defined by {@link #compareTo(SemVersion)}) follows
19 19 * the SemVer 2.0.0 precedence rules:
20 20 * <ul>
21 21 * <li>Compare {@code major}, then {@code minor}, then {@code patch}.</li>
22 22 * <li>Pre-release versions have lower precedence than the corresponding
23 23 * normal version.</li>
24 24 * <li>Build metadata does not affect precedence.</li>
25 25 * </ul>
26 26 *
27 27 * Public API does not use {@code null} for semantic values:
28 28 * {@link Optional} is used for pre-release and build metadata.
29 29 */
30 30 public record SemVersion(
31 31 int major,
32 32 int minor,
33 33 int patch,
34 34 Optional<String> preRelease,
35 35 Optional<String> buildMetadata) implements Comparable<SemVersion> {
36 36
37 37 // Pattern close to the official SemVer 2.0.0 recommendation.
38 38 // Groups:
39 39 // 1 - major
40 40 // 2 - minor
41 41 // 3 - patch
42 42 // 4 - pre-release (without '-')
43 43 // 5 - build metadata (without '+')
44 44 private static final Pattern SEMVER_PATTERN = Pattern.compile(
45 45 "^(0|[1-9]\\d*)" + // major
46 46 "\\.(0|[1-9]\\d*)" + // minor
47 47 "\\.(0|[1-9]\\d*)" + // patch
48 48 "(?:-((?:0|[1-9]\\d*|[0-9A-Za-z-][0-9A-Za-z-]*)" +
49 49 "(?:\\.(?:0|[1-9]\\d*|[0-9A-Za-z-][0-9A-Za-z-]*))*" +
50 50 "))?" + // pre-release
51 51 "(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$" // build metadata
52 52 );
53 53
54 54 /**
55 55 * Compact constructor with basic invariants.
56 56 */
57 57 public SemVersion {
58 58 if (major < 0 || minor < 0 || patch < 0) {
59 59 throw new IllegalArgumentException("Version numbers must be >= 0");
60 60 }
61 61 // Be tolerant internally, but never expose null Optionals.
62 62 preRelease = (preRelease != null) ? preRelease : Optional.empty();
63 63 buildMetadata = (buildMetadata != null) ? buildMetadata : Optional.empty();
64 64 }
65 65
66 66 /**
67 67 * Creates a version without pre-release and build metadata.
68 68 *
69 69 * @param major non-negative MAJOR number
70 70 * @param minor non-negative MINOR number
71 71 * @param patch non-negative PATCH number
72 72 */
73 73 public static SemVersion of(int major, int minor, int patch) {
74 74 return new SemVersion(major, minor, patch, Optional.empty(), Optional.empty());
75 75 }
76 76
77 77 /**
78 78 * Creates a version from components.
79 79 *
80 80 * @param major non-negative MAJOR number
81 81 * @param minor non-negative MINOR number
82 82 * @param patch non-negative PATCH number
83 83 * @param preRelease optional pre-release part (without '-')
84 84 * @param buildMetadata optional build metadata (without '+')
85 85 */
86 86 public static SemVersion of(
87 87 int major,
88 88 int minor,
89 89 int patch,
90 90 Optional<String> preRelease,
91 91 Optional<String> buildMetadata) {
92 92 return new SemVersion(
93 93 major,
94 94 minor,
95 95 patch,
96 96 Objects.requireNonNull(preRelease, "preRelease"),
97 97 Objects.requireNonNull(buildMetadata, "buildMetadata"));
98 98 }
99 99
100 100 /**
101 101 * Parses a SemVer string of the form
102 102 * {@code MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]}.
103 103 *
104 104 * @param value the string to parse (must not be {@code null})
105 105 * @return a new {@code SemVersion}
106 106 * @throws IllegalArgumentException if the string is not a valid SemVer
107 107 */
108 108 public static SemVersion parse(String value) {
109 109 Objects.requireNonNull(value, "value");
110 110
111 111 Matcher m = SEMVER_PATTERN.matcher(value);
112 112 if (!m.matches()) {
113 113 throw new IllegalArgumentException("Invalid SemVer: " + value);
114 114 }
115 115
116 116 int major = Integer.parseInt(m.group(1));
117 117 int minor = Integer.parseInt(m.group(2));
118 118 int patch = Integer.parseInt(m.group(3));
119 119
120 120 String pre = m.group(4);
121 121 String meta = m.group(5);
122 122
123 123 Optional<String> preOpt = Optional.ofNullable(pre);
124 124 Optional<String> metaOpt = Optional.ofNullable(meta);
125 125
126 126 // Extra validation for numeric pre-release identifiers (no leading zeros)
127 127 preOpt.ifPresent(SemVersion::validatePreReleaseIdentifiers);
128 128
129 129 return new SemVersion(major, minor, patch, preOpt, metaOpt);
130 130 }
131 131
132 132 private static void validatePreReleaseIdentifiers(String preRelease) {
133 133 String[] parts = preRelease.split("\\.");
134 134 for (String p : parts) {
135 135 if (isNumericIdentifier(p)) {
136 136 // numeric identifiers must not have leading zeros (except "0")
137 137 if (p.length() > 1 && p.charAt(0) == '0') {
138 138 throw new IllegalArgumentException(
139 139 "Numeric pre-release identifier must not contain leading zeros: " + p);
140 140 }
141 141 }
142 142 }
143 143 }
144 144
145 145 private static boolean isNumericIdentifier(String s) {
146 146 int len = s.length();
147 147 if (len == 0) {
148 148 return false;
149 149 }
150 150 for (int i = 0; i < len; i++) {
151 151 char c = s.charAt(i);
152 152 if (c < '0' || c > '9') {
153 153 return false;
154 154 }
155 155 }
156 156 return true;
157 157 }
158 158
159 159 /**
160 160 * Returns {@code true} if this version has a pre-release part.
161 161 *
162 162 * @return {@code true} if {@link #preRelease()} is present
163 163 */
164 164 public boolean isPreRelease() {
165 165 return preRelease().isPresent();
166 166 }
167 167
168 168 /**
169 169 * Returns {@code true} if this version is considered "stable".
170 170 * <p>
171 171 * By convention:
172 172 * <ul>
173 173 * <li>MAJOR must be greater than 0</li>
174 174 * <li>No pre-release part is present</li>
175 175 * </ul>
176 176 *
177 177 * @return {@code true} if this version is a stable release
178 178 */
179 179 public boolean isStable() {
180 180 return !isPreRelease() && major() > 0;
181 181 }
182 182
183 183 /**
184 184 * Returns {@code true} if this version has strictly lower precedence
185 185 * than the given {@code other} version according to
186 186 * {@link #compareTo(SemVersion)}.
187 187 *
188 188 * @param other the version to compare to
189 189 * @return {@code true} if {@code this.compareTo(other) < 0}
190 190 */
191 191 public boolean isBefore(SemVersion other) {
192 192 return compareTo(other) < 0;
193 193 }
194 194
195 195 /**
196 196 * Returns {@code true} if this version has strictly higher precedence
197 197 * than the given {@code other} version according to
198 198 * {@link #compareTo(SemVersion)}.
199 199 *
200 200 * @param other the version to compare to
201 201 * @return {@code true} if {@code this.compareTo(other) > 0}
202 202 */
203 203 public boolean isAfter(SemVersion other) {
204 204 return compareTo(other) > 0;
205 205 }
206 206
207 207 /**
208 208 * Canonical SemVer string representation.
209 209 * <p>
210 210 * This method is also used by Jackson during serialization.
211 211 *
212 212 * @return canonical SemVer string, e.g. {@code "1.2.3-alpha+build.1"}
213 213 */
214 214 @JsonValue
215 215 public String asString() {
216 216 StringBuilder sb = new StringBuilder()
217 217 .append(major).append('.')
218 218 .append(minor).append('.')
219 219 .append(patch);
220 220
221 221 preRelease.ifPresent(pr -> sb.append('-').append(pr));
222 222 buildMetadata.ifPresent(md -> sb.append('+').append(md));
223 223
224 224 return sb.toString();
225 225 }
226 226
227 227 /**
228 228 * Creates a {@code SemVersion} from a canonical SemVer string.
229 229 * <p>
230 230 * Jackson will use this factory method when deserializing from a JSON string.
231 231 *
232 232 * @param value canonical SemVer string
233 233 * @return parsed {@code SemVersion}
234 234 */
235 235 @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
236 236 public static SemVersion fromJson(String value) {
237 237 return parse(value);
238 238 }
239 239
240 240 /**
241 241 * Compares this version to another {@link SemVersion} according to the
242 242 * SemVer 2.0.0 precedence rules.
243 243 * <p>
244 244 * Build metadata is ignored for ordering.
245 245 */
246 246 @Override
247 247 public int compareTo(SemVersion other) {
248 248 // 1. major / minor / patch
249 249 int c = Integer.compare(this.major, other.major);
250 250 if (c != 0)
251 251 return c;
252 252
253 253 c = Integer.compare(this.minor, other.minor);
254 254 if (c != 0)
255 255 return c;
256 256
257 257 c = Integer.compare(this.patch, other.patch);
258 258 if (c != 0)
259 259 return c;
260 260
261 261 // 2. pre-release (absence > presence)
262 262 boolean thisHasPre = this.preRelease.isPresent();
263 263 boolean otherHasPre = other.preRelease.isPresent();
264 264
265 265 if (!thisHasPre && !otherHasPre) {
266 266 return 0;
267 267 } else if (!thisHasPre) {
268 268 // normal version > pre-release
269 269 return 1;
270 270 } else if (!otherHasPre) {
271 271 return -1;
272 272 }
273 273
274 274 // 3. both have pre-release: compare identifiers
275 275 return comparePreRelease(this.preRelease.get(), other.preRelease.get());
276 276 }
277 277
278 278 private static int comparePreRelease(String a, String b) {
279 279 String[] aParts = a.split("\\.");
280 280 String[] bParts = b.split("\\.");
281 281
282 282 int len = Math.min(aParts.length, bParts.length);
283 283 for (int i = 0; i < len; i++) {
284 284 String ai = aParts[i];
285 285 String bi = bParts[i];
286 286
287 287 boolean aNum = isNumericIdentifier(ai);
288 288 boolean bNum = isNumericIdentifier(bi);
289 289
290 290 if (aNum && bNum) {
291 291 int aiVal = Integer.parseInt(ai);
292 292 int biVal = Integer.parseInt(bi);
293 293 int c = Integer.compare(aiVal, biVal);
294 294 if (c != 0)
295 295 return c;
296 296 } else if (aNum && !bNum) {
297 297 // numeric identifiers have lower precedence than non-numeric
298 298 return -1;
299 299 } else if (!aNum && bNum) {
300 300 return 1;
301 301 } else {
302 302 int c = ai.compareTo(bi);
303 303 if (c != 0)
304 304 return c;
305 305 }
306 306 }
307 307
308 308 // If all common identifiers are equal, the shorter one has lower precedence
309 309 return Integer.compare(aParts.length, bParts.length);
310 310 }
311 311
312 312 @Override
313 313 public String toString() {
314 314 return asString();
315 315 }
316 316 }
@@ -1,54 +1,54
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.lang;
2 2
3 3 import java.util.regex.Pattern;
4 4
5 5 import org.eclipse.jdt.annotation.NonNullByDefault;
6 6 import org.gradle.api.provider.Provider;
7 7
8 8 @NonNullByDefault
9 9 public class Strings {
10 10
11 11 private static final Pattern firstLetter = Pattern.compile("^\\w");
12 12
13 13 public static String capitalize(String string) {
14 14 return string == null ? null
15 15 : string.length() == 0 ? string
16 16 : firstLetter.matcher(string).replaceFirst(m -> m.group().toUpperCase());
17 17 }
18 18
19 19 public static String toCamelCase(String name) {
20 20 if (name == null || name.isEmpty())
21 21 return name;
22 22 StringBuilder out = new StringBuilder(name.length());
23 23 boolean up = false;
24 24 boolean first = true;
25 25 for (int i = 0; i < name.length(); i++) {
26 26 char c = name.charAt(i);
27 27 switch (c) {
28 28 case '-', '_', ' ', '.' -> up = true;
29 29 default -> {
30 30 out.append(
31 31 first ? Character.toLowerCase(c)
32 32 : up ? Character.toUpperCase(c): c);
33 33 up = false;
34 34 first = false;
35 35 }
36 36 }
37 37 }
38 38 return out.toString();
39 39 }
40 40
41 41 public static void argumentNotNullOrEmpty(String value, String argumentName) {
42 42 if (value == null || value.length() == 0)
43 43 throw new IllegalArgumentException(String.format("Argument %s can't be null or empty", argumentName));
44 44 }
45 45
46 46 public static String asString(Object value) {
47 47 if (value == null)
48 48 return null;
49 49 if (value instanceof Provider<?> provider)
50 50 return asString(provider.get());
51 51 else
52 52 return value.toString();
53 53 }
54 54 }
@@ -1,98 +1,98
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.lang;
2 2
3 3 import java.text.MessageFormat;
4 4 import java.util.Iterator;
5 5 import java.util.Spliterators;
6 6 import java.util.Optional;
7 7 import java.util.function.Supplier;
8 8 import java.util.stream.Stream;
9 9 import java.util.stream.StreamSupport;
10 10
11 11 import org.gradle.api.provider.Provider;
12 12
13 13 public final class Values {
14 14
15 15 private Values() {
16 16 }
17 17
18 18 /**
19 19 * Converts the supplied value to a string.
20 20 */
21 21 public static String toString(Object value) {
22 22 if (value == null) {
23 23 return null;
24 24 } else if (value instanceof String string) {
25 25 return string;
26 26 } else if (value instanceof Provider<?> provider) {
27 27 return toString(provider.getOrNull());
28 28 } else if (value instanceof Supplier<?> supplier) {
29 29 return toString(supplier.get());
30 30 } else {
31 31 return value.toString();
32 32 }
33 33 }
34 34
35 35 public static <T> Stream<T> stream(Iterator<T> remaining) {
36 36 return StreamSupport.stream(
37 37 Spliterators.spliteratorUnknownSize(remaining, 0),
38 38 false);
39 39 }
40 40
41 41 public static <T> Optional<T> take(Iterator<T> iterator) {
42 42 return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty();
43 43 }
44 44
45 45 public static <T> Iterable<T> iterable(T[] values) {
46 46 return () -> new ArrayIterator<>(values);
47 47 }
48 48
49 49 public static <T> Optional<T> optional(Provider<T> provider) {
50 50 return provider.isPresent() ? Optional.of(provider.get()) : Optional.empty();
51 51 }
52 52
53 53 public static <T> T required(Provider<T> provider, String providerName) {
54 54 if (!provider.isPresent())
55 55 throw new IllegalStateException(
56 56 MessageFormat.format("The value for the '{0}' provider must be specified", providerName));
57 57 return provider.get();
58 58 }
59 59
60 60 public static boolean parseBoolean(Object value) {
61 61 if (value instanceof Boolean) {
62 62 return (Boolean) value;
63 63 } else {
64 64 var text = toString(value);
65 65 switch (text != null ? text.toLowerCase() : "") {
66 66 case "true", "yes", "1" -> {
67 67 return true;
68 68 }
69 69 case "false", "no", "0", "" -> {
70 70 return false;
71 71 }
72 72 default -> throw new IllegalArgumentException(
73 73 MessageFormat.format("Cannot parse boolean value from ''{0}''", text));
74 74 }
75 75 }
76 76 }
77 77
78 78 private static class ArrayIterator<T> implements Iterator<T> {
79 79 private final T[] data;
80 80
81 81 private int pos = 0;
82 82
83 83 ArrayIterator(T[] data) {
84 84 this.data = data;
85 85 }
86 86
87 87 @Override
88 88 public boolean hasNext() {
89 89 return pos < data.length;
90 90 }
91 91
92 92 @Override
93 93 public T next() {
94 94 return data[pos++];
95 95 }
96 96 }
97 97
98 98 }
@@ -1,16 +1,14
1 package org.implab.gradle.common.utils.os;
1 package org.implab.gradle.common.core.os;
2 2
3 import org.implab.gradle.common.utils.OperatingSystem;
4
5 public class FreeBsd extends GenericSystem{
3 class FreeBsd extends GenericSystem{
6 4
7 5 FreeBsd(String name, String version) {
8 6 super(name, version);
9 7 }
10 8
11 9 @Override
12 10 public String family() {
13 11 return OperatingSystem.FREE_BSD_FAMILY;
14 12 }
15 13
16 14 }
@@ -1,64 +1,63
1 package org.implab.gradle.common.utils.os;
1 package org.implab.gradle.common.core.os;
2 2
3 3 import java.io.File;
4 4 import java.util.Arrays;
5 5 import java.util.Optional;
6 6 import java.util.function.Function;
7 7 import java.util.regex.Pattern;
8 8 import java.util.stream.Stream;
9 9
10 import org.implab.gradle.common.utils.OperatingSystem;
11 import org.implab.gradle.common.utils.Values;
10 import org.implab.gradle.common.core.lang.Values;
12 11
13 12 class GenericSystem implements OperatingSystem {
14 13
15 14 private final String name;
16 15
17 16 private final String version;
18 17
19 18 GenericSystem(String name, String version) {
20 19 this.name = name;
21 20 this.version = version;
22 21 }
23 22
24 23 public String getPathVar() {
25 24 return "PATH";
26 25 }
27 26
28 27 @Override
29 28 public String family() {
30 29 return OperatingSystem.UNKNOWN_FAMILY;
31 30 }
32 31
33 32 @Override
34 33 public String name() {
35 34 return name;
36 35 }
37 36
38 37 @Override
39 38 public String version() {
40 39 return version;
41 40 }
42 41
43 42 protected Stream<File> getPath() {
44 43 String path = System.getenv(getPathVar());
45 44 return path == null
46 45 ? Stream.empty()
47 46 : Arrays.stream(path.split(Pattern.quote(File.pathSeparator)))
48 47 .map(File::new);
49 48 }
50 49
51 50 protected Function<File, Stream<File>> candidates(String cmd) {
52 51 return base -> Stream.of(new File(base, cmd));
53 52 }
54 53
55 54 @Override
56 55 public Optional<File> which(String cmd) {
57 56 return getPath().flatMap(candidates(cmd)).filter(File::isFile).findAny();
58 57 }
59 58
60 59 @Override
61 60 public Optional<File> which(String cmd, Iterable<? extends File> paths) {
62 61 return Values.stream(paths.iterator()).flatMap(candidates(cmd)).filter(File::isFile).findAny();
63 62 }
64 63 }
@@ -1,16 +1,14
1 package org.implab.gradle.common.utils.os;
1 package org.implab.gradle.common.core.os;
2 2
3 import org.implab.gradle.common.utils.OperatingSystem;
4
5 public class Linux extends GenericSystem {
3 class Linux extends GenericSystem {
6 4
7 5 Linux(String name, String version) {
8 6 super(name, version);
9 7 }
10 8
11 9 @Override
12 10 public String family() {
13 11 return OperatingSystem.LINUX_FAMILY;
14 12 }
15 13
16 14 }
@@ -1,15 +1,13
1 package org.implab.gradle.common.utils.os;
1 package org.implab.gradle.common.core.os;
2 2
3 import org.implab.gradle.common.utils.OperatingSystem;
4
5 public class MacOs extends GenericSystem {
3 class MacOs extends GenericSystem {
6 4
7 5 MacOs(String name, String version) {
8 6 super(name, version);
9 7 }
10 8
11 9 @Override
12 10 public String family() {
13 11 return OperatingSystem.MAC_OS_FAMILY;
14 12 }
15 13 }
@@ -1,33 +1,31
1 package org.implab.gradle.common.utils;
1 package org.implab.gradle.common.core.os;
2 2
3 3 import java.io.File;
4 4 import java.util.Optional;
5 5
6 import org.implab.gradle.common.utils.os.SystemResolver;
7
8 6 public interface OperatingSystem {
9 7
10 8 public static final String WINDOWS_FAMILY = "windows";
11 9
12 10 public static final String LINUX_FAMILY = "linux";
13 11
14 12 public static final String FREE_BSD_FAMILY = "freebsd";
15 13
16 14 public static final String MAC_OS_FAMILY = "os x";
17 15
18 16 public static final String UNKNOWN_FAMILY = "unknown";
19 17
20 18 String family();
21 19
22 20 String name();
23 21
24 22 String version();
25 23
26 24 Optional<File> which(String cmd);
27 25
28 26 Optional<File> which(String cmd, Iterable<? extends File> paths);
29 27
30 28 public static OperatingSystem current() {
31 29 return SystemResolver.current();
32 30 }
33 31 }
@@ -1,37 +1,36
1 package org.implab.gradle.common.utils.os;
1 package org.implab.gradle.common.core.os;
2 2
3 import org.implab.gradle.common.utils.LazyValue;
4 import org.implab.gradle.common.utils.OperatingSystem;
3 import org.implab.gradle.common.core.lang.LazyValue;
5 4
6 public class SystemResolver {
5 class SystemResolver {
7 6
8 7 private final static LazyValue<OperatingSystem> current = new LazyValue<>(SystemResolver::resolveCurrent);
9 8
10 9 private static OperatingSystem resolveCurrent() {
11 10 var osName = System.getProperty("os.name");
12 11 var osVersion = System.getProperty("os.version");
13 12
14 13 return forName(osName, osVersion);
15 14 }
16 15
17 16 public static OperatingSystem current() {
18 17 return current.get();
19 18 }
20 19
21 20 public static OperatingSystem forName(String os, String version) {
22 21 var osName = os.toLowerCase();
23 22
24 23 if (osName.contains("windows")) {
25 24 return new Windows(osName, version);
26 25 } else if (osName.contains("mac os x") || osName.contains("darwin") || osName.contains("osx")) {
27 26 return new MacOs(osName, version);
28 27 } else if (osName.contains("linux")) {
29 28 return new Linux(osName, version);
30 29 } else if (osName.contains("freebsd")) {
31 30 return new FreeBsd(osName, version);
32 31 } else {
33 32 // Not strictly true
34 33 return new GenericSystem(osName, version);
35 34 }
36 35 }
37 36 }
@@ -1,26 +1,24
1 package org.implab.gradle.common.utils.os;
1 package org.implab.gradle.common.core.os;
2 2
3 3 import java.io.File;
4 4 import java.util.function.Function;
5 5 import java.util.stream.Stream;
6 6
7 import org.implab.gradle.common.utils.OperatingSystem;
8
9 7 class Windows extends GenericSystem {
10 8
11 9 private Stream<String> exeSuffixes = Stream.of(".cmd", ".bat", ".exe");
12 10
13 11 Windows(String name, String version) {
14 12 super(name, version);
15 13 }
16 14
17 15 @Override
18 16 public String family() {
19 17 return OperatingSystem.WINDOWS_FAMILY;
20 18 }
21 19
22 20 @Override
23 21 protected Function<File, Stream<File>> candidates(String cmd) {
24 22 return base -> exeSuffixes.map(suffix -> new File(base, cmd + suffix));
25 23 }
26 24 }
@@ -1,61 +1,61
1 package org.implab.gradle.common.dsl;
1 package org.implab.gradle.common.exec.dsl;
2 2
3 3 import java.io.File;
4 4 import java.io.InputStream;
5 5 import java.util.Optional;
6 6 import java.util.function.Supplier;
7 7
8 8 import org.gradle.api.provider.Provider;
9 9 import org.gradle.util.Configurable;
10 import org.implab.gradle.common.exec.RedirectFrom;
11 import org.implab.gradle.common.utils.Closures;
10 import org.implab.gradle.common.core.lang.Closures;
11 import org.implab.gradle.common.exec.runtime.RedirectFrom;
12 12
13 13 import groovy.lang.Closure;
14 14
15 15 public class RedirectFromSpec implements Configurable<RedirectFromSpec> {
16 16 private Supplier<RedirectFrom> streamRedirect;
17 17
18 18 public boolean isRedirected() {
19 19 return streamRedirect != null;
20 20 }
21 21
22 22 public Optional<RedirectFrom> getRedirection() {
23 23 return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty();
24 24 }
25 25
26 26 public void fromFile(File file) {
27 27 streamRedirect = () -> RedirectFrom.file(file);
28 28 }
29 29
30 30 public void fromFile(Provider<File> fileProvider) {
31 31 streamRedirect = fileProvider.map(RedirectFrom::file)::getOrNull;
32 32 }
33 33
34 34 public void fromStream(InputStream stream) {
35 35 streamRedirect = () -> RedirectFrom.stream(stream);
36 36 }
37 37
38 38 public void fromStream(Provider<InputStream> streamProvider) {
39 39 streamRedirect = streamProvider.map(RedirectFrom::stream)::getOrNull;
40 40 }
41 41
42 42 public void from(Object input) {
43 43 if (input instanceof Provider<?> inputProvider) {
44 44 streamRedirect = inputProvider.map(RedirectFrom::any)::get;
45 45 } else {
46 46 streamRedirect = () -> RedirectFrom.any(input);
47 47 }
48 48 }
49 49
50 50 public void empty() {
51 51 streamRedirect = () -> null;
52 52 }
53 53
54 54 @Override
55 55 public RedirectFromSpec configure(Closure cl) {
56 56 Closures.apply(cl, this);
57 57 return this;
58 58 }
59 59
60 60
61 61 }
@@ -1,75 +1,75
1 package org.implab.gradle.common.dsl;
1 package org.implab.gradle.common.exec.dsl;
2 2
3 3 import java.io.File;
4 4 import java.io.OutputStream;
5 5 import java.util.Optional;
6 6 import java.util.function.Consumer;
7 7 import java.util.function.Supplier;
8 8
9 9 import org.eclipse.jdt.annotation.NonNullByDefault;
10 10 import org.eclipse.jdt.annotation.Nullable;
11 11 import org.gradle.api.provider.Provider;
12 12 import org.gradle.util.Configurable;
13 import org.implab.gradle.common.exec.RedirectTo;
14 import org.implab.gradle.common.utils.Closures;
13 import org.implab.gradle.common.core.lang.Closures;
14 import org.implab.gradle.common.exec.runtime.RedirectTo;
15 15
16 16 import groovy.lang.Closure;
17 17
18 18 @NonNullByDefault
19 19 public class RedirectToSpec implements Configurable<RedirectToSpec> {
20 20 private Supplier<RedirectTo> streamRedirect;
21 21
22 22 public boolean isRedirected() {
23 23 return getRedirection().isPresent();
24 24 }
25 25
26 26 public Optional<RedirectTo> getRedirection() {
27 27 return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty();
28 28 }
29 29
30 30 public @Nullable RedirectTo getRedirectionOrNull() {
31 31 return streamRedirect != null ? streamRedirect.get() : null;
32 32 }
33 33
34 34 public void toFile(File file) {
35 35 streamRedirect = () -> RedirectTo.file(file);
36 36 }
37 37
38 38 public void toFile(Provider<File> fileProvider) {
39 39 streamRedirect = fileProvider.map(RedirectTo::file)::getOrNull;
40 40 }
41 41
42 42 public void toStream(OutputStream stream) {
43 43 streamRedirect = () -> RedirectTo.stream(stream);
44 44 }
45 45
46 46 public void toStream(Provider<OutputStream> streamProvider) {
47 47 streamRedirect = streamProvider.map(RedirectTo::stream)::getOrNull;
48 48 }
49 49
50 50 public void to(Object output) {
51 51 if (output instanceof Provider<?> outputProvider) {
52 52 streamRedirect = outputProvider.map(RedirectTo::any)::get;
53 53 } else {
54 54 streamRedirect = () -> RedirectTo.any(output);
55 55 }
56 56 }
57 57
58 58 public void eachLine(Consumer<String> consumer) {
59 59 streamRedirect = () -> RedirectTo.eachLine(consumer);
60 60 }
61 61
62 62 public void allText(Consumer<String> consumer) {
63 63 streamRedirect = () -> RedirectTo.allText(consumer);
64 64 }
65 65
66 66 public void discard() {
67 67 streamRedirect = () -> null;
68 68 }
69 69
70 70 @Override
71 71 public RedirectToSpec configure(Closure cl) {
72 72 Closures.apply(cl, this);
73 73 return this;
74 74 }
75 75 }
@@ -1,26 +1,26
1 package org.implab.gradle.common.dsl;
1 package org.implab.gradle.common.exec.dsl;
2 2
3 3 import java.util.stream.Stream;
4 4
5 5 import org.gradle.api.provider.ListProperty;
6 import org.implab.gradle.common.utils.Properties;
6 import org.implab.gradle.common.core.gradle.Properties;
7 7
8 8 public interface TaskCommandSpecMixin {
9 9 ListProperty<String> getCommandLine();
10 10
11 11 default void commandLine(Object arg0, Object... args) {
12 12 getCommandLine().empty();
13 13 Properties.mergeList(
14 14 getCommandLine(),
15 15 () -> Stream.concat(Stream.of(arg0), Stream.of(args)).iterator(),
16 16 Object::toString);
17 17 }
18 18
19 19 default void args(Object arg0, Object... args) {
20 20 Properties.mergeList(
21 21 getCommandLine(),
22 22 () -> Stream.concat(Stream.of(arg0), Stream.of(args)).iterator(),
23 23 Object::toString);
24 24 }
25 25
26 26 }
@@ -1,45 +1,45
1 package org.implab.gradle.common.dsl;
1 package org.implab.gradle.common.exec.dsl;
2 2
3 3 import java.util.Map;
4 4
5 5 import org.gradle.api.Action;
6 6 import org.gradle.api.file.DirectoryProperty;
7 7 import org.gradle.api.provider.MapProperty;
8 8 import org.gradle.api.provider.Property;
9 import org.implab.gradle.common.utils.Closures;
10 import org.implab.gradle.common.utils.Properties;
9 import org.implab.gradle.common.core.gradle.Properties;
10 import org.implab.gradle.common.core.lang.Closures;
11 11
12 12 import groovy.lang.Closure;
13 13
14 14 /**
15 15 * Configuration properties of the execution shell. This object specifies a
16 16 * working directory and environment variables for the processes started within
17 17 * this shell.
18 18 */
19 19 public interface TaskEnvSpecMixin {
20 20
21 21 /** Inherit environment from current process */
22 22 Property<Boolean> getInheritEnvironment();
23 23
24 24 /** Working directory */
25 25 DirectoryProperty getWorkingDirectory();
26 26
27 27 /** Environment variables */
28 28 MapProperty<String, String> getEnvironment();
29 29
30 30 /**
31 31 * Configures the environment variable using the specified action. The
32 32 * action is called when the value is calculated;
33 33 *
34 34 * <p>
35 35 * The configuration action is called immediately. To support lazy evaluation,
36 36 * properties may be assigned to providers.
37 37 */
38 38 default void env(Action<Map<String, Object>> configure) {
39 39 Properties.configureMap(getEnvironment(), configure, Object::toString);
40 40 }
41 41
42 42 default void env(Closure<?> configure) {
43 43 env(Closures.action(configure));
44 44 }
45 45 }
@@ -1,12 +1,12
1 package org.implab.gradle.common.dsl;
1 package org.implab.gradle.common.exec.dsl;
2 2
3 3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 4
5 5 @NonNullByDefault
6 6 public interface TaskPipeSpecMixin {
7 7 RedirectToSpec getStdout();
8 8
9 9 RedirectToSpec getStderr();
10 10
11 11 RedirectFromSpec getStdin();
12 12 }
@@ -1,52 +1,52
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.model;
2 2
3 3 import static java.util.Objects.requireNonNull;
4 4
5 5 import java.util.Optional;
6 6
7 7 import org.eclipse.jdt.annotation.NonNullByDefault;
8 import org.implab.gradle.common.utils.Values;
8 import org.implab.gradle.common.core.lang.Values;
9 9
10 10 @NonNullByDefault
11 11 public interface CommandArgumentsBuilder<S extends CommandArgumentsBuilder<S>> {
12 12
13 13 S self();
14 14
15 15 default S flag(String name, boolean value) {
16 16 requireNonNull(name, "Parameter name cannot be null");
17 17 if (value)
18 18 addArguments(name);
19 19 return self();
20 20 }
21 21
22 22 default S param(String name, String value) {
23 23 requireNonNull(name, "Parameter name cannot be null");
24 24 requireNonNull(value, "Parameter value cannot be null");
25 25 if (value != null && value.length() > 0)
26 26 addArguments(name, value);
27 27 return self();
28 28 }
29 29
30 30 default S param(String name, Optional<String> value) {
31 31 value.ifPresent(v -> param(name, v));
32 32 return self();
33 33 }
34 34
35 35 /** Adds the specified arguments to this builder */
36 36 S addArguments(String... args);
37 37
38 38 default S addArguments(Iterable<String> args) {
39 39 for (String arg : args)
40 40 addArguments(arg);
41 41
42 42 return self();
43 43 }
44 44
45 45 /** Replaces arguments in the builder with the specified one. */
46 46 default S arguments(String... args) {
47 47 return arguments(Values.iterable(args));
48 48 }
49 49
50 50 /** Replaces arguments in the builder with the specified one. */
51 51 S arguments(Iterable<String> args);
52 52 }
@@ -1,63 +1,63
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.model;
2 2
3 3 import java.io.File;
4 4
5 5 import org.eclipse.jdt.annotation.NonNullByDefault;
6 import org.implab.gradle.common.utils.Values;
6 import org.implab.gradle.common.core.lang.Values;
7 7
8 8 /** Command builder interface, used to specify the executable and parameters */
9 9 @NonNullByDefault
10 10 public interface CommandBuilder extends CommandArgumentsBuilder<CommandBuilder> {
11 11
12 12 @Override
13 13 default CommandBuilder self() {
14 14 return this;
15 15 }
16 16
17 17 /** Sets the executable, the parameters are left intact. */
18 18 CommandBuilder executable(String executable);
19 19
20 20 default CommandBuilder executable(File executable) {
21 21 executable(executable.toString());
22 22 return this;
23 23 }
24 24
25 25 /**
26 26 * Sets the specified executable and parameters, old executable and parameters
27 27 * are discarded.
28 28 */
29 29 default CommandBuilder commandLine(String executable, String... args) {
30 30 return executable(executable)
31 31 .arguments(args);
32 32 }
33 33
34 34 /**
35 35 * Sets the specified executable and parameters, old executable and parameters
36 36 * are discarded.
37 37 *
38 38 * @param command The command line. Must contain at least one element
39 39 * (executable).
40 40 *
41 41 */
42 42 default CommandBuilder commandLine(Iterable<? extends String> command) {
43 43 var iterator = command.iterator();
44 44
45 45 // set executable
46 46 executable(Values.take(iterator).orElseThrow());
47 47 // cleat arguments
48 48 arguments();
49 49
50 50 // set new arguments
51 51 while (iterator.hasNext())
52 52 addArguments(iterator.next());
53 53
54 54 return this;
55 55 }
56 56
57 57 default CommandBuilder from(CommandSpec commandSpec) {
58 58 return executable(commandSpec.executable())
59 59 .arguments(commandSpec.arguments());
60 60 }
61 61
62 62 CommandSpec build();
63 63 }
@@ -1,23 +1,23
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.model;
2 2
3 3 import java.util.List;
4 4 import java.util.stream.Stream;
5 5
6 6 import org.eclipse.jdt.annotation.NonNullByDefault;
7 7
8 8 @NonNullByDefault
9 9 public interface CommandSpec {
10 10
11 11 String executable();
12 12
13 13 List<String> arguments();
14 14
15 15 default List<String> commandLine() {
16 16 return Stream.concat(Stream.of(executable()), arguments().stream()).toList();
17 17 }
18 18
19 19 public static CommandBuilder builder() {
20 20 return new CommandSpecRecord.Builder();
21 21 }
22 22
23 23 }
@@ -1,51 +1,51
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.model;
2 2
3 3 import java.util.List;
4 4
5 5 import org.eclipse.jdt.annotation.NonNullByDefault;
6 6
7 7 import static java.util.Objects.requireNonNull;
8 8
9 9 import java.util.ArrayList;
10 10
11 11 @NonNullByDefault
12 12 public record CommandSpecRecord(String executable, List<String> arguments) implements CommandSpec {
13 13
14 14 static class Builder implements CommandBuilder {
15 15
16 16 private String executable;
17 17
18 18 private List<String> arguments = new ArrayList<>();
19 19
20 20 @Override
21 21 public CommandBuilder executable(String executable) {
22 22 requireNonNull(executable, "cmd can't be null");
23 23 this.executable = executable;
24 24 return this;
25 25 }
26 26
27 27 @Override
28 28 public CommandBuilder arguments(Iterable<String> args) {
29 29 requireNonNull(args, "Args must not be null");
30 30 arguments.clear();
31 31 for (var arg : args)
32 32 arguments.add(arg);
33 33 return this;
34 34 }
35 35
36 36 @Override
37 37 public CommandBuilder addArguments(String... args) {
38 38 for (var arg : args)
39 39 arguments.add(requireNonNull(arg, "arguments element shouldn't be null"));
40 40
41 41 return this;
42 42 }
43 43
44 44 @Override
45 45 public CommandSpec build() {
46 46 requireNonNull(executable, "Executable must be specified");
47 47 return new CommandSpecRecord(executable, arguments);
48 48 }
49 49
50 50 }
51 51 }
@@ -1,17 +1,17
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.model;
2 2
3 3 import java.io.File;
4 4 import java.util.Map;
5 5 import java.util.Optional;
6 6
7 7 import org.eclipse.jdt.annotation.NonNullByDefault;
8 8
9 9 @NonNullByDefault
10 10 public interface EnvironmentSpec {
11 11
12 12 boolean inheritEnvironment();
13 13
14 14 Map<String,String> environment();
15 15
16 16 Optional<File> workingDirectory();
17 17 }
@@ -1,20 +1,23
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.model;
2
3 import org.implab.gradle.common.exec.runtime.RedirectFrom;
4 import org.implab.gradle.common.exec.runtime.RedirectTo;
2 5
3 6 import java.util.Optional;
4 7
5 8 import org.eclipse.jdt.annotation.NonNullByDefault;
6 9
7 10 /**
8 11 * The execution shell uses this specification when starting
9 12 * a new process. The shell may check for the specified
10 13 * redirections and apply them when launching the process.
11 14 * The exact moment they are applied is at the shell’s discretion.
12 15 */
13 16 @NonNullByDefault
14 17 public interface PipeSpec {
15 18 Optional<RedirectTo> stdout();
16 19
17 20 Optional<RedirectTo> stderr();
18 21
19 22 Optional<RedirectFrom> stdin();
20 23 }
@@ -1,212 +1,214
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.util.HashMap;
4 4 import java.util.Map;
5 5 import java.util.Optional;
6 6 import java.util.concurrent.CompletableFuture;
7 7
8 8 import org.eclipse.jdt.annotation.NonNullByDefault;
9 9 import org.eclipse.jdt.annotation.Nullable;
10 import org.implab.gradle.common.exec.model.EnvironmentSpec;
11 import org.implab.gradle.common.exec.model.PipeSpec;
10 12
11 13 import java.io.File;
12 14 import java.io.IOException;
13 15
14 16 import static java.util.Objects.requireNonNull;
15 17
16 18 /** Command line builder */
17 19 @NonNullByDefault
18 20 public abstract class AbstractExecBuilder<CS> implements ShellExec {
19 21
20 22 private boolean inheritEnvironment = true;
21 23
22 24 private final Map<String, String> environment = new HashMap<>();
23 25
24 26 private @Nullable File directory;
25 27
26 28 private RedirectFrom inputRedirect;
27 29
28 30 private RedirectTo outputRedirect;
29 31
30 32 private RedirectTo errorRedirect;
31 33
32 34 private final CS command;
33 35
34 36 protected AbstractExecBuilder(CS command) {
35 37 this.command = command;
36 38 }
37 39
38 40
39 41 /** Sets the working directory */
40 42 @Override
41 43 public ShellExec workingDirectory(File directory) {
42 44 requireNonNull(directory, "directory parameter can't be null");
43 45
44 46 this.directory = directory;
45 47 return this;
46 48 }
47 49
48 50 @Override
49 51 public ShellExec workingDirectory(Optional<? extends File> directory) {
50 52 requireNonNull(directory, "directory parameter can't be null");
51 53
52 54 this.directory = directory.orElse(null);
53 55 return this;
54 56 }
55 57
56 58 @Override
57 59 public ShellExec inheritEnvironment(boolean inherit) {
58 60 this.inheritEnvironment = inherit;
59 61 return this;
60 62 }
61 63
62 64 /**
63 65 * Sets the environment value. The value cannot be null.
64 66 *
65 67 * @param envVar The name of the environment variable
66 68 * @param value The value to set,
67 69 */
68 70 @Override
69 71 public ShellExec putEnvironment(String envVar, String value) {
70 72 requireNonNull(value, "Value can't be null");
71 73 requireNonNull(envVar, "envVar parameter can't be null");
72 74
73 75 environment.put(envVar, value);
74 76 return this;
75 77 }
76 78
77 79 @Override
78 80 public ShellExec environment(Map<String, ? extends String> env) {
79 81 requireNonNull(env, "env parameter can't be null");
80 82
81 83 environment.clear();
82 84 environment.putAll(env);
83 85 return this;
84 86 }
85 87
86 88 /**
87 89 * Sets redirection for the stdin, {@link RedirectFrom} will be applied
88 90 * every time the process is started.
89 91 *
90 92 * <p>
91 93 * If redirection
92 94 */
93 95 @Override
94 96 public ShellExec stdin(RedirectFrom from) {
95 97 requireNonNull(from, "from parameter can't be null");
96 98
97 99 inputRedirect = from;
98 100 return this;
99 101 }
100 102
101 103 @Override
102 104 public ShellExec stdin(Optional<? extends RedirectFrom> from) {
103 105 requireNonNull(from, "from parameter can't be null");
104 106 inputRedirect = from.orElse(null);
105 107 return this;
106 108 }
107 109
108 110 /**
109 111 * Sets redirection for the stdout, {@link RedirectTo} will be applied
110 112 * every time the process is started.
111 113 */
112 114 @Override
113 115 public ShellExec stdout(RedirectTo out) {
114 116 requireNonNull(out, "out parameter can't be null");
115 117 outputRedirect = out;
116 118 return this;
117 119 }
118 120
119 121 @Override
120 122 public ShellExec stdout(Optional<? extends RedirectTo> to) {
121 123 requireNonNull(to, "from parameter can't be null");
122 124 outputRedirect = to.orElse(null);
123 125 return this;
124 126 }
125 127
126 128 /**
127 129 * Sets redirection for the stderr, {@link RedirectTo} will be applied
128 130 * every time the process is started.
129 131 */
130 132 @Override
131 133 public ShellExec stderr(RedirectTo err) {
132 134 requireNonNull(err, "err parameter can't be null");
133 135 errorRedirect = err;
134 136 return this;
135 137 }
136 138
137 139 @Override
138 140 public ShellExec stderr(Optional<? extends RedirectTo> to) {
139 141 requireNonNull(to, "from parameter can't be null");
140 142 errorRedirect = to.orElse(null);
141 143 return this;
142 144 }
143 145
144 146 @Override
145 147 public ShellExec from(PipeSpec pipeSpec) {
146 148 ShellExec.super.from(pipeSpec);
147 149 return this;
148 150 }
149 151
150 152 @Override
151 153 public ShellExec from(EnvironmentSpec environmentSpec) {
152 154 ShellExec.super.from(environmentSpec);
153 155 return this;
154 156 }
155 157
156 158 /** Implement this function to */
157 159 protected abstract CompletableFuture<Integer> startInternal(
158 160 CS command,
159 161 EnvironmentSpec environment,
160 162 PipeSpec redirect) throws IOException;
161 163
162 164 /**
163 165 * Creates and starts new process and returns {@link CompletableFuture}. The
164 166 * process may be a remote process, or a shell process or etc.
165 167 *
166 168 * @return
167 169 * @throws IOException
168 170 */
169 171 public CompletableFuture<Integer> exec() throws IOException {
170 172 return startInternal(
171 173 command,
172 174 new SelfEnvironmentSpec(),
173 175 new SelfPipeSpec());
174 176 }
175 177
176 178 private class SelfEnvironmentSpec implements EnvironmentSpec {
177 179 @Override
178 180 public boolean inheritEnvironment() {
179 181 return inheritEnvironment;
180 182 }
181 183
182 184 @Override
183 185 public Map<String, String> environment() {
184 186 return environment;
185 187 }
186 188
187 189 @Override
188 190 public Optional<File> workingDirectory() {
189 191 return Optional.ofNullable(directory);
190 192 }
191 193 }
192 194
193 195 private class SelfPipeSpec implements PipeSpec {
194 196
195 197 @Override
196 198 public Optional<RedirectTo> stdout() {
197 199 return Optional.ofNullable(outputRedirect);
198 200 }
199 201
200 202 @Override
201 203 public Optional<RedirectTo> stderr() {
202 204 return Optional.ofNullable(errorRedirect);
203 205 }
204 206
205 207 @Override
206 208 public Optional<RedirectFrom> stdin() {
207 209 return Optional.ofNullable(inputRedirect);
208 210 }
209 211
210 212 }
211 213
212 214 }
@@ -1,43 +1,46
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.io.ByteArrayInputStream;
4 4 import java.io.IOException;
5 5 import java.nio.charset.StandardCharsets;
6 6 import java.util.concurrent.CompletableFuture;
7 7 import java.util.stream.Collectors;
8 8
9 9 import org.eclipse.jdt.annotation.NonNullByDefault;
10 import org.implab.gradle.common.exec.model.CommandSpec;
11 import org.implab.gradle.common.exec.model.EnvironmentSpec;
12 import org.implab.gradle.common.exec.model.PipeSpec;
10 13
11 14 @NonNullByDefault
12 15 class EchoExecBuilder extends AbstractExecBuilder<CommandSpec> {
13 16
14 17 private final boolean echoToStderr;
15 18
16 19 public EchoExecBuilder(CommandSpec command, boolean echoToStderr) {
17 20 super(command);
18 21 this.echoToStderr = echoToStderr;
19 22 }
20 23
21 24 @Override
22 25 protected CompletableFuture<Integer> startInternal(
23 26 CommandSpec command,
24 27 EnvironmentSpec environment,
25 28 PipeSpec redirect) throws IOException {
26 29
27 30 var outputRedirect = echoToStderr ? redirect.stderr() : redirect.stdout();
28 31
29 32 return outputRedirect
30 33 .map(to -> {
31 34 var bytes = String
32 35 .format(
33 36 "exec: %s",
34 37 command.commandLine().stream().collect(Collectors.joining(" ")))
35 38 .getBytes(StandardCharsets.UTF_8);
36 39
37 40 return to.redirect(new ByteArrayInputStream(bytes))
38 41 .thenApply((x) -> 0);
39 42 })
40 43 .orElse(CompletableFuture.completedFuture(0));
41 44 }
42 45
43 46 }
@@ -1,33 +1,34
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.io.File;
4 4 import java.util.Map;
5 5 import java.util.Optional;
6 6
7 7 import org.eclipse.jdt.annotation.NonNullByDefault;
8 import org.implab.gradle.common.exec.model.EnvironmentSpec;
8 9
9 10 @NonNullByDefault
10 11 public interface EnvironmentBuilder {
11 12 /** Sets the specified environment variable */
12 13 EnvironmentBuilder putEnvironment(String envVar, String value);
13 14
14 15 /** Replaces environment with the supplied one */
15 16 EnvironmentBuilder environment(Map<String, ? extends String> env);
16 17
17 18 /**
18 19 * Enables or disables environment inheritance for the child process
19 20 */
20 21 EnvironmentBuilder inheritEnvironment(boolean inherit);
21 22
22 23 /** Specifies the working directory */
23 24 EnvironmentBuilder workingDirectory(File directory);
24 25
25 26 /** Specifies the working directory */
26 27 EnvironmentBuilder workingDirectory(Optional<? extends File> directory);
27 28
28 29 /** Copies the supplied environment to this one */
29 30 default EnvironmentBuilder from(EnvironmentSpec environmentSpec) {
30 31 return environment(environmentSpec.environment())
31 32 .workingDirectory(environmentSpec.workingDirectory());
32 33 }
33 34 }
@@ -1,27 +1,28
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.util.Optional;
4 4
5 5 import org.eclipse.jdt.annotation.NonNullByDefault;
6 import org.implab.gradle.common.exec.model.PipeSpec;
6 7
7 8 @NonNullByDefault
8 9 public interface PipeBuilder {
9 10 PipeBuilder stdin(RedirectFrom from);
10 11
11 12 PipeBuilder stdin(Optional<? extends RedirectFrom> from);
12 13
13 14 PipeBuilder stdout(RedirectTo to);
14 15
15 16 PipeBuilder stdout(Optional<? extends RedirectTo> to);
16 17
17 18 PipeBuilder stderr(RedirectTo to);
18 19
19 20 PipeBuilder stderr(Optional<? extends RedirectTo> to);
20 21
21 22 default PipeBuilder from(PipeSpec pipeSpec) {
22 23 return stdin(pipeSpec.stdin())
23 24 .stdout(pipeSpec.stdout())
24 25 .stderr(pipeSpec.stderr());
25 26 }
26 27
27 28 }
@@ -1,56 +1,56
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.io.File;
4 4 import java.io.FileInputStream;
5 5 import java.io.InputStream;
6 6 import java.io.OutputStream;
7 7 import java.util.concurrent.CompletableFuture;
8 8
9 9 import org.eclipse.jdt.annotation.NonNullByDefault;
10 10
11 11 /**
12 12 * Describes how to redirect input streams. This interface is used to configure
13 13 * lazy redirection. {@link #redirect(OutputStream)} is called when the process
14 14 * is started. Before the process is started the redirection isn't invoked and
15 15 * no resources are allocated or used.
16 16 */
17 17 @NonNullByDefault
18 18 public interface RedirectFrom {
19 19 CompletableFuture<Void> redirect(OutputStream to);
20 20
21 21 /**
22 22 * Read file contents and redirect it to the output stream.
23 23 */
24 24 public static RedirectFrom file(final File file) {
25 25 return to -> CompletableFuture.runAsync(() -> {
26 26 try (var from = new FileInputStream(file); to) {
27 27 from.transferTo(to);
28 28 } catch (Exception e) {
29 29 // silence!
30 30 }
31 31 });
32 32 }
33 33
34 34 public static RedirectFrom stream(final InputStream from) {
35 35 return to -> CompletableFuture.runAsync(() -> {
36 36 try (from; to) {
37 37 from.transferTo(to);
38 38 } catch (Exception e) {
39 39 // silence!
40 40 }
41 41 });
42 42 }
43 43
44 44 public static RedirectFrom any(final Object output) {
45 45 if (output instanceof File f) {
46 46 return file(f);
47 47 } else if (output instanceof InputStream stm) {
48 48 return stream(stm);
49 49 } else if (output instanceof RedirectFrom self) {
50 50 return self;
51 51 } else {
52 52 throw new IllegalArgumentException("The specified argument type isn't supported: " + output.getClass());
53 53 }
54 54 }
55 55
56 56 }
@@ -1,111 +1,111
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.io.File;
4 4 import java.io.FileOutputStream;
5 5 import java.io.InputStream;
6 6 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 10 import java.util.stream.Collectors;
11 11 import java.util.stream.Stream;
12 12
13 13 import org.eclipse.jdt.annotation.NonNullByDefault;
14 14
15 15 /**
16 16 * Redirection specification for the {@link InputStream}. Redirection is invoked
17 17 * when the {@link InputStream} becomes available, for example, on process
18 18 * start. Before the process is started the redirection isn't invoked and no
19 19 * resources are allocated or used.
20 20 */
21 21 @NonNullByDefault
22 22 public interface RedirectTo {
23 23 CompletableFuture<Void> redirect(InputStream from);
24 24
25 25 public interface StringConsumer extends Consumer<String> {
26 26 default void complete() {
27 27 }
28 28 }
29 29
30 30 /** Creates a redirect to the specified consumer */
31 31 public static RedirectTo eachLine(final Consumer<String> consumer) {
32 32 return consumer(new StringConsumer() {
33 33 @Override
34 34 public void accept(String s) {
35 35 consumer.accept(s);
36 36 }
37 37 });
38 38 }
39 39
40 40 public static RedirectTo allText(final Consumer<String> consumer) {
41 41 return consumer(new StringConsumer() {
42 42 final Stream.Builder<String> builder = Stream.builder();
43 43
44 44 @Override
45 45 public void accept(String s) {
46 46 builder.accept(s);
47 47 }
48 48
49 49 @Override
50 50 public void complete() {
51 51 consumer.accept(builder.build().collect(Collectors.joining("\n")));
52 52 }
53 53 });
54 54 }
55 55
56 56 /** Creates a redirect to the specified consumer */
57 57 public static RedirectTo consumer(final StringConsumer consumer) {
58 58 return (src) -> CompletableFuture.runAsync(() -> {
59 59 try (Scanner sc = new Scanner(src)) {
60 60 while (sc.hasNextLine()) {
61 61 consumer.accept(sc.nextLine());
62 62 }
63 63 consumer.complete();
64 64 }
65 65 });
66 66 }
67 67
68 68 /**
69 69 * Creates a redirect to the specified file. There will be opened the output
70 70 * stream in this redirection and original stream will be transferred to this
71 71 * this output stream.
72 72 */
73 73 public static RedirectTo file(final File file) {
74 74 return src -> CompletableFuture.runAsync(() -> {
75 75 try (src; OutputStream out = new FileOutputStream(file)) {
76 76 src.transferTo(out);
77 77 } catch (Exception e) {
78 78 // silence!
79 79 }
80 80 });
81 81 }
82 82
83 83 /** Creates a redirect to the specified output stream. */
84 84 public static RedirectTo stream(final OutputStream dest) {
85 85 return src -> CompletableFuture.runAsync(() -> {
86 86 try (dest; src) {
87 87 src.transferTo(dest);
88 88 } catch (Exception e) {
89 89 // silence!
90 90 }
91 91 });
92 92 }
93 93
94 94 /**
95 95 * Creates the redirection to the specified destination, actual type of
96 96 * redirection will be determined from the type of the output object.
97 97 */
98 98 public static RedirectTo any(final Object output) {
99 99 if (output instanceof StringConsumer fn) {
100 100 return consumer(s -> fn.accept(s));
101 101 } else if (output instanceof File f) {
102 102 return file(f);
103 103 } else if (output instanceof OutputStream stm) {
104 104 return stream(stm);
105 105 } else if (output instanceof RedirectTo self) {
106 106 return self;
107 107 } else {
108 108 throw new IllegalArgumentException("The specified argument type isn't supported: " + output.getClass());
109 109 }
110 110 }
111 } No newline at end of file
111 }
@@ -1,29 +1,30
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 import org.implab.gradle.common.exec.model.CommandSpec;
4 5
5 6 @NonNullByDefault
6 7 public interface Shell {
7 8
8 9 ShellExec create(CommandSpec spec);
9 10
10 11 default ShellTool tool(String executable) {
11 12 return new ShellTool(executable, this);
12 13 }
13 14
14 15 /** Creates a new shell to start processes using ProcessBuilder. */
15 16 static Shell system() {
16 17 return spec -> new SystemExecBuilder(spec);
17 18 }
18 19
19 20 /** Creates a stub shell which echoes the specified command line. */
20 21 static Shell echo() {
21 22 return spec -> new EchoExecBuilder(spec, false);
22 23 }
23 24
24 25 /** Creates a stub shell which echoes the specified command line. */
25 26 static Shell echoStderr() {
26 27 return spec -> new EchoExecBuilder(spec, false);
27 28 }
28 29
29 30 }
@@ -1,38 +1,38
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.io.IOException;
4 4 import java.util.ArrayList;
5 5 import java.util.List;
6 6 import java.util.concurrent.CompletableFuture;
7 7
8 8 import org.eclipse.jdt.annotation.NonNullByDefault;
9 import org.implab.gradle.common.utils.Exceptions;
9 import org.implab.gradle.common.core.lang.Exceptions;
10 10
11 11 @NonNullByDefault
12 12 public interface ShellExec extends PipeBuilder, EnvironmentBuilder {
13 13
14 14 default CompletableFuture<List<String>> readAllLines() throws IOException {
15 15 List<String> lines = new ArrayList<>();
16 16 stdout(RedirectTo.consumer(lines::add));
17 17
18 18 return exec()
19 19 .thenAccept(Exceptions.unchecked(this::assertExitCode))
20 20 .thenApply(v -> lines);
21 21 }
22 22
23 23 default CompletableFuture<String> readAllText() throws IOException {
24 24 List<String> lines = new ArrayList<>();
25 25 stdout(RedirectTo.consumer(lines::add));
26 26
27 27 return exec()
28 28 .thenAccept(Exceptions.unchecked(this::assertExitCode))
29 29 .thenApply(v -> String.join("\n", lines));
30 30 }
31 31
32 32 default void assertExitCode(Integer code) throws IOException {
33 33 if (code != 0)
34 34 throw new IOException(String.format("The process is terminated with code %d", code));
35 35 }
36 36
37 37 CompletableFuture<Integer> exec() throws IOException;
38 38 }
@@ -1,40 +1,42
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import org.eclipse.jdt.annotation.NonNullByDefault;
4 4 import org.gradle.api.Action;
5 import org.implab.gradle.common.exec.model.CommandArgumentsBuilder;
6 import org.implab.gradle.common.exec.model.CommandSpec;
5 7
6 8 @NonNullByDefault
7 9 public class ShellTool {
8 10
9 11 private final Shell shell;
10 12
11 13 private final String executable;
12 14
13 15 ShellTool(String executable, Shell shell) {
14 16 this.shell = shell;
15 17 this.executable = executable;
16 18 }
17 19
18 20 public ShellExec arguments(String... args) {
19 21 return shell.create(CommandSpec.builder()
20 22 .executable(executable)
21 23 .arguments(args)
22 24 .build());
23 25 }
24 26
25 27 public ShellExec arguments(Iterable<String> args) {
26 28 return shell.create(CommandSpec.builder()
27 29 .executable(executable)
28 30 .arguments(args)
29 31 .build());
30 32 }
31 33
32 34 public ShellExec arguments(Action<CommandArgumentsBuilder<?>> args) {
33 35 var builder = CommandSpec.builder()
34 36 .executable(executable);
35 37 args.execute(builder);
36 38
37 39 return shell.create(builder.build());
38 40 }
39 41
40 42 }
@@ -1,52 +1,55
1 package org.implab.gradle.common.exec;
1 package org.implab.gradle.common.exec.runtime;
2 2
3 3 import java.io.IOException;
4 4 import java.lang.ProcessBuilder.Redirect;
5 5 import java.util.ArrayList;
6 6 import java.util.concurrent.CompletableFuture;
7 7
8 8 import org.eclipse.jdt.annotation.NonNullByDefault;
9 import org.implab.gradle.common.exec.model.CommandSpec;
10 import org.implab.gradle.common.exec.model.EnvironmentSpec;
11 import org.implab.gradle.common.exec.model.PipeSpec;
9 12
10 13 @NonNullByDefault
11 14 class SystemExecBuilder extends AbstractExecBuilder<CommandSpec> {
12 15 SystemExecBuilder(CommandSpec command) {
13 16 super(command);
14 17 }
15 18
16 19 @Override
17 20 protected CompletableFuture<Integer> startInternal(
18 21 CommandSpec command,
19 22 EnvironmentSpec environment,
20 23 PipeSpec redirect) throws IOException {
21 24
22 25 var builder = new ProcessBuilder(command.commandLine());
23 26
24 27 environment.workingDirectory().ifPresent(builder::directory);
25 28
26 29 // if the env isn't inherited we need to clear it
27 30 if (!environment.inheritEnvironment())
28 31 builder.environment().clear();
29 32
30 33 builder.environment().putAll(environment.environment());
31 34
32 35 var tasks = new ArrayList<CompletableFuture<?>>();
33 36
34 37 if (!redirect.stdout().isPresent())
35 38 builder.redirectOutput(Redirect.DISCARD);
36 39 if (!redirect.stderr().isPresent())
37 40 builder.redirectError(Redirect.DISCARD);
38 41
39 42 // run process
40 43 var proc = builder.start();
41 44
42 45 tasks.add(proc.onExit());
43 46
44 47 redirect.stdin().map(from -> from.redirect(proc.getOutputStream())).ifPresent(tasks::add);
45 48 redirect.stdout().map(to -> to.redirect(proc.getInputStream())).ifPresent(tasks::add);
46 49 redirect.stderr().map(to -> to.redirect(proc.getErrorStream())).ifPresent(tasks::add);
47 50
48 51 return CompletableFuture
49 52 .allOf(tasks.toArray(new CompletableFuture<?>[0]))
50 53 .thenApply(t -> proc.exitValue());
51 54 }
52 55 }
@@ -1,108 +1,108
1 package org.implab.gradle.common.tasks;
1 package org.implab.gradle.common.exec.tasks;
2 2
3 3 import java.io.IOException;
4 4 import java.util.Map;
5 5 import java.util.Optional;
6 6 import java.util.concurrent.ExecutionException;
7 7
8 8 import org.gradle.api.DefaultTask;
9 9 import org.gradle.api.file.DirectoryProperty;
10 10 import org.gradle.api.provider.MapProperty;
11 11 import org.gradle.api.provider.Property;
12 12 import org.gradle.api.tasks.Internal;
13 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;
14 import org.implab.gradle.common.core.lang.Exceptions;
15 import org.implab.gradle.common.core.lang.Values;
16 import org.implab.gradle.common.exec.dsl.RedirectFromSpec;
17 import org.implab.gradle.common.exec.dsl.RedirectToSpec;
18 import org.implab.gradle.common.exec.dsl.TaskEnvSpecMixin;
19 import org.implab.gradle.common.exec.dsl.TaskPipeSpecMixin;
20 import org.implab.gradle.common.exec.runtime.RedirectTo;
21 import org.implab.gradle.common.exec.runtime.ShellExec;
22 22
23 23 public abstract class AbstractShellExecTask
24 24 extends DefaultTask
25 25 implements TaskPipeSpecMixin, TaskEnvSpecMixin {
26 26
27 27 private final RedirectToSpec redirectStderr = new RedirectToSpec();
28 28
29 29 private final RedirectToSpec redirectStdout = new RedirectToSpec();
30 30
31 31 private final RedirectFromSpec redirectStdin = new RedirectFromSpec();
32 32
33 33 @Internal
34 34 @Override
35 35 public abstract Property<Boolean> getInheritEnvironment();
36 36
37 37 @Internal
38 38 @Override
39 39 public abstract DirectoryProperty getWorkingDirectory();
40 40
41 41 @Internal
42 42 @Override
43 43 public abstract MapProperty<String, String> getEnvironment();
44 44
45 45 /**
46 46 * STDIN redirection, if not specified, no input will be passed to the command
47 47 */
48 48 @Internal
49 49 @Override
50 50 public RedirectFromSpec getStdin() {
51 51 return redirectStdin;
52 52 }
53 53
54 54 /**
55 55 * STDOUT redirection, if not specified, redirected to logger::info
56 56 */
57 57 @Internal
58 58 @Override
59 59 public RedirectToSpec getStdout() {
60 60 return redirectStdout;
61 61 }
62 62
63 63 /**
64 64 * STDERR redirection, if not specified, redirected to logger::error
65 65 */
66 66 @Internal
67 67 @Override
68 68 public RedirectToSpec getStderr() {
69 69 return redirectStderr;
70 70 }
71 71
72 72 protected abstract ShellExec execBuilder();
73 73
74 74 protected Optional<RedirectTo> conventionalStderr() {
75 75 return Optional.of(RedirectTo.eachLine(getLogger()::error));
76 76 }
77 77
78 78 protected Optional<RedirectTo> conventionalStdout() {
79 79 return Optional.of(RedirectTo.eachLine(getLogger()::info));
80 80 }
81 81
82 82 @TaskAction
83 83 public final void run() throws IOException, InterruptedException, ExecutionException {
84 84 // create new shell process
85 85 var execBuilder = execBuilder();
86 86
87 87 // configure environment
88 88 execBuilder
89 89 .workingDirectory(Values.optional(getWorkingDirectory().getAsFile()))
90 90 .environment(getEnvironment().getOrElse(Map.of()));
91 91
92 92 // configure redirects
93 93 execBuilder
94 94 .stdout(getStdout().getRedirection().or(this::conventionalStdout))
95 95 .stderr(getStderr().getRedirection().or(this::conventionalStderr))
96 96 .stdin(getStdin().getRedirection());
97 97
98 98 // execute
99 99 execBuilder.exec()
100 100 .thenAccept(Exceptions.unchecked(this::assertExitCode))
101 101 .join();
102 102 }
103 103
104 104 protected void assertExitCode(Integer code) throws IOException {
105 105 if (code != 0)
106 106 throw new IOException(String.format("The process is terminated with code %s", code));
107 107 }
108 } No newline at end of file
108 }
@@ -1,31 +1,31
1 package org.implab.gradle.common.tasks;
1 package org.implab.gradle.common.exec.tasks;
2 2
3 3 import org.gradle.api.provider.ListProperty;
4 4 import org.gradle.api.provider.Property;
5 5 import org.gradle.api.tasks.Internal;
6 import org.implab.gradle.common.dsl.TaskCommandSpecMixin;
7 import org.implab.gradle.common.exec.CommandSpec;
8 import org.implab.gradle.common.exec.Shell;
9 import org.implab.gradle.common.exec.ShellExec;
6 import org.implab.gradle.common.exec.dsl.TaskCommandSpecMixin;
7 import org.implab.gradle.common.exec.model.CommandSpec;
8 import org.implab.gradle.common.exec.runtime.Shell;
9 import org.implab.gradle.common.exec.runtime.ShellExec;
10 10
11 11 public abstract class ShellExecTask
12 12 extends AbstractShellExecTask
13 13 implements TaskCommandSpecMixin {
14 14
15 15 @Internal
16 16 public abstract Property<Shell> getShell();
17 17
18 18 @Internal
19 19 @Override
20 20 public abstract ListProperty<String> getCommandLine();
21 21
22 22
23 23 @Override
24 24 protected ShellExec execBuilder() {
25 25 return getShell().get()
26 26 .create(CommandSpec.builder()
27 27 .commandLine(getCommandLine().get())
28 28 .build());
29 29 }
30 30
31 31 }
@@ -1,142 +1,142
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.core;
2 2
3 3 import java.io.File;
4 4 import java.io.IOException;
5 5 import java.util.Map;
6 6 import java.util.function.Consumer;
7 7 import java.util.function.Function;
8 8
9 import org.implab.gradle.common.utils.LazyValue;
9 import org.implab.gradle.common.core.lang.LazyValue;
10 10
11 11 import com.fasterxml.jackson.annotation.JsonInclude.Include;
12 12 import com.fasterxml.jackson.core.JsonProcessingException;
13 13 import com.fasterxml.jackson.core.type.TypeReference;
14 14 import com.fasterxml.jackson.core.util.DefaultIndenter;
15 15 import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
16 16 import com.fasterxml.jackson.databind.JsonMappingException;
17 17 import com.fasterxml.jackson.databind.ObjectMapper;
18 18 import com.fasterxml.jackson.databind.SerializationFeature;
19 19 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
20 20 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
21 21
22 22 public class Json {
23 23
24 24 private final static LazyValue<ObjectMapper> mapper = new LazyValue<>(Json::createMapper);
25 25
26 26 private static ObjectMapper createMapper() {
27 27 DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
28 28 prettyPrinter.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE);
29 29
30 30 return new ObjectMapper()
31 31 .registerModule(new Jdk8Module())
32 32 .registerModule(new JavaTimeModule())
33 33 .setSerializationInclusion(Include.NON_EMPTY)
34 34 .enable(SerializationFeature.INDENT_OUTPUT)
35 35 .setDefaultPrettyPrinter(prettyPrinter);
36 36 }
37 37
38 38 public static ObjectMapper jsonMapper() {
39 39 return mapper.get();
40 40 }
41 41
42 42 public static Consumer<Object> fileWriter(File file) {
43 43 return value -> {
44 44 try {
45 45 jsonMapper().writeValue(file, value);
46 46 } catch (IOException e) {
47 47 throw new SerializationException(e);
48 48 }
49 49 };
50 50 }
51 51
52 52 public static void write(File file, Object value) {
53 53 try {
54 54 jsonMapper().writeValue(file, value);
55 55 } catch (IOException e) {
56 56 throw new SerializationException(e);
57 57 }
58 58 }
59 59
60 60 public static String stringify(Object value) {
61 61 try {
62 62 return jsonMapper().writeValueAsString(value);
63 63 } catch (JsonProcessingException e) {
64 64 throw new SerializationException(e);
65 65 }
66 66 }
67 67
68 68 public static <T> T parse(String json, Class<T> clazz) {
69 69 try {
70 70 return jsonMapper().readValue(json, clazz);
71 71 } catch (JsonProcessingException e) {
72 72 throw new SerializationException(e);
73 73 }
74 74 }
75 75
76 76 public static <T> T parse(String json, TypeReference<T> type) {
77 77 try {
78 78 return jsonMapper().readValue(json, type);
79 79 } catch (JsonProcessingException e) {
80 80 throw new SerializationException(e);
81 81 }
82 82 }
83 83
84 84 public static <V> Map<String, V> parseMap(String json, Class<V> clazz) {
85 85 return parse(json, new TypeReference<>() {
86 86 });
87 87 }
88 88
89 89 public static <V> Function<String, Map<String, V>> mapParser(Class<V> clazz) {
90 90 return json -> parseMap(json, clazz);
91 91 }
92 92
93 93 public static <T> T read(File file, TypeReference<T> type) {
94 94 try {
95 95 return jsonMapper().readValue(file, type);
96 96 } catch (IOException e) {
97 97 throw new SerializationException(e);
98 98 }
99 99 }
100 100
101 101 public static <V> Map<String, V> readMap(File file, Class<V> clazz) {
102 102 return read(file, new TypeReference<>() {
103 103 });
104 104 }
105 105
106 106 public static <T> T updateValue(T target, Object source) {
107 107 try {
108 108 return jsonMapper().updateValue(target, source);
109 109 } catch (JsonMappingException e) {
110 110 throw new SerializationException(e);
111 111 }
112 112 }
113 113
114 114 public static <T> T convertValue(Object source, Class<T> clazz) {
115 115 var mapper = jsonMapper();
116 116 var buf = mapper.getSerializerProvider().bufferForValueConversion(jsonMapper());
117 117
118 118 try {
119 119 mapper.writeValue(buf, source);
120 120 return mapper.readValue(buf.asParser(), clazz);
121 121 } catch (IOException e) {
122 122 throw new SerializationException(e);
123 123 }
124 124 }
125 125
126 126 public static class SerializationException extends RuntimeException {
127 127 public SerializationException() {
128 128 }
129 129
130 130 public SerializationException(String message) {
131 131 super(message);
132 132 }
133 133
134 134 public SerializationException(Throwable cause) {
135 135 super(cause);
136 136 }
137 137
138 138 public SerializationException(String message, Throwable cause) {
139 139 super(message, cause);
140 140 }
141 141 }
142 142 }
@@ -1,66 +1,66
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.core;
2 2
3 3 import java.util.Arrays;
4 4 import java.util.LinkedHashSet;
5 5 import java.util.Set;
6 6 import java.util.function.Predicate;
7 7
8 8 import org.gradle.api.Action;
9 import org.implab.gradle.common.utils.Closures;
9 import org.implab.gradle.common.core.lang.Closures;
10 10
11 11 import groovy.lang.Closure;
12 12
13 13 /**
14 14 * ΠŸΡ€ΠΎΡΡ‚Π°Ρ include/exclude-маска ΠΏΠΎ ΠΊΠ»ΡŽΡ‡Π°ΠΌ Π²Π΅Ρ€Ρ…Π½Π΅Π³ΠΎ уровня.
15 15 */
16 16 public class MapImportSpec {
17 17
18 18 private final Set<String> includes = new LinkedHashSet<>();
19 19 private final Set<String> excludes = new LinkedHashSet<>();
20 20
21 21 public void include(String... keys) {
22 22 includes.addAll(Arrays.asList(keys));
23 23 }
24 24
25 25 public void exclude(String... keys) {
26 26 excludes.addAll(Arrays.asList(keys));
27 27 }
28 28
29 29 public Set<String> getIncludes() {
30 30 return includes;
31 31 }
32 32
33 33 public Set<String> getExcludes() {
34 34 return excludes;
35 35 }
36 36
37 37 public boolean hasIncludes() {
38 38 return !includes.isEmpty();
39 39 }
40 40
41 41 public boolean isExcluded(String key) {
42 42 return excludes.contains(key);
43 43 }
44 44
45 45 public boolean isIncluded(String key) {
46 46 return !hasIncludes() || includes.contains(key);
47 47 }
48 48
49 49 public boolean matches(String key) {
50 50 return isIncluded(key) && !isExcluded(key);
51 51 }
52 52
53 53 public Predicate<String> toPredicate() {
54 54 return this::matches;
55 55 }
56 56
57 57 public static Predicate<String> buildPredicate(Action<? super MapImportSpec> action) {
58 58 var spec = new MapImportSpec();
59 59 action.execute(spec);
60 60 return spec.toPredicate();
61 61 }
62 62
63 63 public static Predicate<String> buildPredicate(Closure<?> closure) {
64 64 return buildPredicate(Closures.action(closure));
65 65 }
66 } No newline at end of file
66 }
@@ -1,29 +1,29
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.dsl;
2 2
3 3
4 4 import org.gradle.api.provider.Provider;
5 5
6 6 import java.util.ArrayList;
7 7 import java.util.Collections;
8 8 import java.util.List;
9 9
10 10
11 11 public class DefaultJsonArraySpec implements JsonArraySpec {
12 12
13 13 private final List<Object> values = new ArrayList<>();
14 14
15 15 @Override
16 16 public void add(Object value) {
17 17 if (value instanceof Provider<?>) {
18 18 throw new IllegalArgumentException(
19 19 "Providers are not allowed inside JSON arrays; " +
20 20 "use top-level metadata.set(\"key\", provider) instead."
21 21 );
22 22 }
23 23 values.add(value);
24 24 }
25 25
26 26 public List<Object> toList() {
27 27 return Collections.unmodifiableList(values);
28 28 }
29 } No newline at end of file
29 }
@@ -1,26 +1,26
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.dsl;
2 2
3 3 import org.gradle.api.provider.Provider;
4 4
5 5 import java.util.Collections;
6 6 import java.util.LinkedHashMap;
7 7 import java.util.Map;
8 8
9 9 public class DefaultJsonObjectSpec implements GroovyObjectSpec {
10 10
11 11 private final Map<String, Object> values = new LinkedHashMap<>();
12 12
13 13 @Override
14 14 public void set(String key, Object value) {
15 15 if (value instanceof Provider<?>) {
16 16 throw new IllegalArgumentException(
17 17 "Providers are not allowed inside nested JSON objects; " +
18 18 "use top-level metadata.set(\"" + key + "\", provider) instead.");
19 19 }
20 20 values.put(key, value);
21 21 }
22 22
23 23 public Map<String, Object> toMap() {
24 24 return Collections.unmodifiableMap(values);
25 25 }
26 } No newline at end of file
26 }
@@ -1,38 +1,38
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.dsl;
2 2
3 3 import java.util.Arrays;
4 4
5 5 import groovy.lang.Closure;
6 6 import groovy.lang.MissingMethodException;
7 7
8 8 public interface GroovyObjectSpec extends JsonObjectSpec {
9 9
10 10 default void propertyMissing(String name, Object value) {
11 11 set(name, value);
12 12 }
13 13
14 14 default Object methodMissing(String name, Object args) {
15 15 Object[] arr = (Object[]) args;
16 16
17 17 // author { ... }, repository { ... }
18 18 if (arr.length == 1 && arr[0] instanceof Closure<?>) {
19 19 DefaultJsonObjectSpec spec = new DefaultJsonObjectSpec();
20 20 Closure<?> cl = (Closure<?>) arr[0];
21 21 cl.setDelegate(spec);
22 22 cl.setResolveStrategy(Closure.DELEGATE_FIRST);
23 23 cl.call();
24 24 set(name, spec.toMap());
25 25 return null;
26 26 } else {
27 27 boolean hasClosure = Arrays.stream(arr)
28 28 .anyMatch(a -> a instanceof Closure<?>);
29 29
30 30 if (!hasClosure) {
31 31 set(name, Arrays.asList(arr));
32 32 return null;
33 33 }
34 34 }
35 35
36 36 throw new MissingMethodException(name, getClass(), arr);
37 37 }
38 38 }
@@ -1,20 +1,20
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.dsl;
2 2
3 3 import org.gradle.api.Action;
4 4
5 5 public interface JsonArraySpec {
6 6
7 7 void add(Object value);
8 8
9 9 default void obj(Action<? super JsonObjectSpec> action) {
10 10 DefaultJsonObjectSpec child = new DefaultJsonObjectSpec();
11 11 action.execute(child);
12 12 add(child.toMap());
13 13 }
14 14
15 15 default void arr(Action<? super JsonArraySpec> action) {
16 16 DefaultJsonArraySpec child = new DefaultJsonArraySpec();
17 17 action.execute(child);
18 18 add(child.toList());
19 19 }
20 } No newline at end of file
20 }
@@ -1,23 +1,24
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.dsl;
2 2
3 3 import java.io.File;
4 4
5 5 import org.gradle.api.Action;
6 6 import org.gradle.api.file.RegularFile;
7 7 import org.gradle.api.provider.Provider;
8 import org.implab.gradle.common.json.core.MapImportSpec;
8 9
9 10 public interface JsonMapSpec extends GroovyObjectSpec {
10 11 void from(File file, Action<? super MapImportSpec> action);
11 12
12 13 default void from(File file) {
13 14 from(file, spec -> {
14 15 });
15 16 }
16 17
17 18 void from(Provider<RegularFile> fileProvider, Action<? super MapImportSpec> action);
18 19
19 20 default void from(Provider<RegularFile> fileProvider) {
20 21 from(fileProvider, spec -> {
21 22 });
22 23 }
23 24 }
@@ -1,20 +1,20
1 package org.implab.gradle.common.json;
1 package org.implab.gradle.common.json.dsl;
2 2
3 3 import org.gradle.api.Action;
4 4
5 5 public interface JsonObjectSpec {
6 6
7 7 void set(String key, Object value);
8 8
9 9 default void obj(String key, Action<? super JsonObjectSpec> action) {
10 10 DefaultJsonObjectSpec child = new DefaultJsonObjectSpec();
11 11 action.execute(child);
12 12 set(key, child.toMap());
13 13 }
14 14
15 15 default void arr(String key, Action<? super JsonArraySpec> action) {
16 16 DefaultJsonArraySpec child = new DefaultJsonArraySpec();
17 17 action.execute(child);
18 18 set(key, child.toList());
19 19 }
20 } No newline at end of file
20 }
@@ -1,181 +1,180
1 package org.implab.gradle.common.tasks;
1 package org.implab.gradle.common.json.tasks;
2 2
3 3 import java.util.LinkedHashMap;
4 4 import java.util.Map;
5 5 import java.util.Objects;
6 6 import java.util.function.Predicate;
7 7 import java.util.stream.Collectors;
8 8
9 9 import javax.inject.Inject;
10 10
11 11 import org.eclipse.jdt.annotation.NonNullByDefault;
12 12 import org.gradle.api.Action;
13 13 import org.gradle.api.DefaultTask;
14 14 import org.gradle.api.file.RegularFile;
15 15 import org.gradle.api.file.RegularFileProperty;
16 16 import org.gradle.api.provider.MapProperty;
17 17 import org.gradle.api.provider.Provider;
18 18 import org.gradle.api.provider.ProviderFactory;
19 19 import org.gradle.api.tasks.Input;
20 20 import org.gradle.api.tasks.Internal;
21 21 import org.gradle.api.tasks.Optional;
22 22 import org.gradle.api.tasks.OutputFile;
23 import org.gradle.api.tasks.SkipWhenEmpty;
24 23 import org.gradle.api.tasks.TaskAction;
25 import org.implab.gradle.common.json.MapImportSpec;
26 import org.implab.gradle.common.json.Json;
27 import org.implab.gradle.common.json.JsonObjectSpec;
28 import org.implab.gradle.common.utils.Closures;
29 import org.implab.gradle.common.utils.Properties;
24 import org.implab.gradle.common.core.gradle.Properties;
25 import org.implab.gradle.common.core.lang.Closures;
26 import org.implab.gradle.common.json.core.Json;
27 import org.implab.gradle.common.json.core.MapImportSpec;
28 import org.implab.gradle.common.json.dsl.JsonObjectSpec;
30 29
31 30 import groovy.lang.Closure;
32 31
33 32 /**
34 33 * A Gradle task that writes JSON content to a file.
35 34 *
36 35 * This task allows you to build up a JSON object through various methods and
37 36 * write it to a file.
38 37 * You can add content directly, import from other JSON files, or merge in map
39 38 * data with optional filtering.
40 39 *
41 40 * <h3>Usage Example:</h3>
42 41 *
43 42 * <pre>
44 43 * tasks.register('generateConfig', WriteJson) {
45 44 * outputFile = file('build/config.json')
46 45 * content {
47 46 * version = '1.0.0'
48 47 * name = 'myapp'
49 48 * }
50 49 * from file('src/base-config.json')
51 50 * putAll providers.provider { ['debug': true] }
52 51 * }
53 52 * </pre>
54 53 *
55 54 * <h3>Properties:</h3>
56 55 * <ul>
57 56 * <li><strong>content</strong> - A map of string keys to object values that
58 57 * will be serialized to JSON</li>
59 58 * <li><strong>outputFile</strong> - The file where the JSON will be
60 59 * written</li>
61 60 * </ul>
62 61 *
63 62 * <h3>Methods:</h3>
64 63 * <ul>
65 64 * <li><strong>content()</strong> - Configure content using a closure or
66 65 * action</li>
67 66 * <li><strong>from()</strong> - Import JSON content from a file with optional
68 67 * key filtering</li>
69 68 * <li><strong>putAll()</strong> - Merge a map provider with optional key
70 69 * filtering</li>
71 70 * </ul>
72 71 *
73 72 * @since 1.0
74 73 */
75 74 @NonNullByDefault
76 75 public abstract class WriteJson extends DefaultTask {
77 76
78 77 private final ProviderFactory providers;
79 78
80 79 private final JsonObjectSpec contentSpec = new JsonObjectSpec() {
81 80 @Override
82 81 public void set(String key, Object value) {
83 82 Properties.putMapEntry(getContent(), key, value);
84 83 }
85 84 };
86 85
87 86 @Input
88 87 @Optional
89 88 public abstract MapProperty<String, Object> getContent();
90 89
91 90 @OutputFile
92 91 public abstract RegularFileProperty getOutputFile();
93 92
94 93 @Internal
95 94 public Provider<String> getJson() {
96 95 return getContent().map(Json::stringify);
97 96 }
98 97
99 98 @Inject
100 99 public WriteJson(ProviderFactory providers) {
101 100 this.providers = providers;
102 101 }
103 102
104 103 public void content(Action<? super JsonObjectSpec> configure) {
105 104 configure.execute(contentSpec);
106 105 }
107 106
108 107 public void content(Closure<?> configure) {
109 108 Closures.apply(configure, contentSpec);
110 109 }
111 110
112 111 public void from(RegularFile file, Closure<?> configure) {
113 112 importContents(file, MapImportSpec.buildPredicate(configure));
114 113 }
115 114
116 115 public void from(RegularFile file, Action<? super MapImportSpec> configure) {
117 116 importContents(file, MapImportSpec.buildPredicate(configure));
118 117 }
119 118
120 119 public void from(RegularFile file) {
121 120 importContents(file, x -> true);
122 121 }
123 122
124 123 public void from(Provider<RegularFile> file, Closure<?> configure) {
125 124 importContents(file, MapImportSpec.buildPredicate(configure));
126 125 }
127 126
128 127 public void from(Provider<RegularFile> file, Action<? super MapImportSpec> configure) {
129 128 importContents(file, MapImportSpec.buildPredicate(configure));
130 129 }
131 130
132 131 public void from(Provider<RegularFile> file) {
133 132 importContents(file, x -> true);
134 133 }
135 134
136 135 public void putAll(Provider<Map<String, Object>> mapProvider, Action<? super MapImportSpec> configure) {
137 136 importMap(mapProvider, MapImportSpec.buildPredicate(configure));
138 137 }
139 138
140 139 public void putAll(Provider<Map<String, Object>> mapProvider, Closure<?> configure) {
141 140 importMap(mapProvider, MapImportSpec.buildPredicate(configure));
142 141 }
143 142
144 143 public void putAll(Provider<Map<String, Object>> mapProvider) {
145 144 importMap(mapProvider, x -> true);
146 145 }
147 146
148 147 private void importContents(Provider<RegularFile> file, Predicate<String> keyPredicate) {
149 148 var jsonProvider = providers.fileContents(file).getAsText()
150 149 .map(Json.mapParser(Object.class)::apply);
151 150
152 151 importMap(jsonProvider, keyPredicate);
153 152 }
154 153
155 154 private void importContents(RegularFile file, Predicate<String> keyPredicate) {
156 155 var jsonProvider = providers.fileContents(file).getAsText()
157 156 .map(Json.mapParser(Object.class)::apply);
158 157
159 158 importMap(jsonProvider, keyPredicate);
160 159 }
161 160
162 161 private void importMap(Provider<Map<String, Object>> mapProvider, Predicate<? super String> keyPredicate) {
163 162 var filteredProvider = mapProvider.map(v -> v.entrySet().stream()
164 163 .filter(pair -> keyPredicate.test(pair.getKey()) && Objects.nonNull(pair.getValue()))
165 164 .collect(Collectors.toMap(
166 165 Map.Entry::getKey,
167 166 Map.Entry::getValue,
168 167 (a, b) -> b,
169 168 LinkedHashMap::new)));
170 169
171 170 getContent().putAll(filteredProvider);
172 171 }
173 172
174 173 @TaskAction
175 174 public void run() {
176 175 var data = getContent().getOrElse(Map.of());
177 176 var file = getOutputFile().getAsFile().get();
178 177 Json.write(file, data);
179 178 }
180 179
181 180 }
@@ -1,136 +1,208
1 package org.implab.gradle.common.files;
1 package org.implab.gradle.common.sources;
2 2
3 3 import java.io.File;
4 4 import java.nio.file.Paths;
5 5 import java.util.HashSet;
6 import java.util.LinkedHashMap;
6 7 import java.util.List;
8 import java.util.Map;
7 9 import java.util.Objects;
8 10 import java.util.Set;
9 11 import java.util.concurrent.Callable;
12 import java.util.function.Function;
10 13 import java.util.stream.Collectors;
11 14
12 15 import javax.inject.Inject;
13 16
17 import org.gradle.api.InvalidUserDataException;
14 18 import org.gradle.api.Named;
15 19 import org.gradle.api.NamedDomainObjectContainer;
20 import org.gradle.api.Task;
21 import org.gradle.api.file.ConfigurableFileCollection;
16 22 import org.gradle.api.file.DirectoryProperty;
17 23 import org.gradle.api.file.FileCollection;
18 24 import org.gradle.api.file.ProjectLayout;
19 25 import org.gradle.api.file.SourceDirectorySet;
20 import org.gradle.api.logging.Logger;
21 import org.gradle.api.logging.Logging;
22 26 import org.gradle.api.model.ObjectFactory;
27 import org.gradle.api.tasks.TaskProvider;
23 28 import org.gradle.util.Configurable;
24 import org.implab.gradle.common.utils.Closures;
29 import org.implab.gradle.common.core.lang.Closures;
25 30
26 31 import groovy.lang.Closure;
27 32
33 /**
34 * A configurable source set abstraction with named output roles.
35 *
36 * <p>
37 * Each instance aggregates multiple {@link SourceDirectorySet source sets}
38 * under a shared name and exposes typed outputs that must be declared up front.
39 * Default locations are {@code src/<name>} for sources and
40 * {@code build/<name>} for outputs, both of which can be customized via the
41 * exposed {@link DirectoryProperty} setters.
42 * </p>
43 *
44 * <p>
45 * Outputs are grouped by roles to make task wiring explicit. A role must be
46 * declared with {@link #declareRoles(String, String...)} (or the synonym
47 * {@link #declareOutputs(String, String...)}) before files can be registered
48 * against it. Attempting to register or retrieve an undeclared role results in
49 * {@link InvalidUserDataException}.
50 * </p>
51 */
28 52 public abstract class GenericSourceSet
29 53 implements Named, Configurable<GenericSourceSet> {
30 private final Logger logger = Logging.getLogger(GenericSourceSet.class);
31
32 54 private final String name;
33 55
34 56 private final NamedDomainObjectContainer<SourceDirectorySet> sourceDirectorySets;
35 57
36 private final NamedDomainObjectContainer<GenericSourceSetOutput> outputs;
58 private final Map<String, ConfigurableFileCollection> outputs;
37 59
38 60 private final FileCollection allOutputs;
39 61
40 62 private final FileCollection allSourceDirectories;
41 63
42 64 private final ObjectFactory objects;
43 65
44 private final Set<String> declaredOutputs = new HashSet<>();
66 private final Set<String> declaredRoles = new HashSet<>();
45 67
46 68 @Inject
47 69 public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) {
48 70 this.name = name;
49 71 this.objects = objects;
50 72
51 73 sourceDirectorySets = objects.domainObjectContainer(
52 74 SourceDirectorySet.class,
53 75 this::createSourceDirectorySet);
54 76
55 outputs = objects.domainObjectContainer(GenericSourceSetOutput.class);
77 outputs = new LinkedHashMap<>();
56 78
57 79 allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider());
58 80
59 81 allOutputs = objects.fileCollection().from(outputsProvider());
60 82
61 outputs.addRule("Register a declared set on demand", this::registerOutputOnDemand);
62
63 83 getSourceSetDir().convention(layout
64 84 .getProjectDirectory()
65 85 .dir(Paths.get("src", name).toString()));
66 86
67 87 getOutputsDir().convention(layout
68 88 .getBuildDirectory()
69 89 .dir(name));
70 90 }
71 91
72 92 @Override
73 93 public String getName() {
74 94 return name;
75 95 }
76 96
97 /**
98 * Base directory for this source set. Defaults to {@code src/<name>} under
99 * the project directory.
100 */
77 101 public abstract DirectoryProperty getSourceSetDir();
78 102
103 /**
104 * Base directory for outputs of this source set. Defaults to
105 * {@code build/<name>}.
106 */
79 107 public abstract DirectoryProperty getOutputsDir();
80 108
109 /**
110 * The container of {@link SourceDirectorySet} instances that belong to this
111 * logical source set.
112 */
81 113 public NamedDomainObjectContainer<SourceDirectorySet> getSets() {
82 114 return sourceDirectorySets;
83 115 }
84 116
85 public NamedDomainObjectContainer<GenericSourceSetOutput> getOutputs() {
86 return outputs;
87 }
88
117 /**
118 * All registered outputs grouped across roles.
119 */
89 120 public FileCollection getAllOutputs() {
90 121 return allOutputs;
91 122 }
92 123
124 /**
125 * All source directories from every contained {@link SourceDirectorySet}.
126 */
93 127 public FileCollection getAllSourceDirectories() {
94 128 return allSourceDirectories;
95 129 }
96 130
97 public FileCollection output(String name) {
98 return getOutputs().getAt(name).getFileCollection();
131 /**
132 * Returns the file collection for the specified output role, creating it
133 * if necessary.
134 *
135 * @throws InvalidUserDataException if the role was not declared
136 */
137 public ConfigurableFileCollection output(String name) {
138 requireDeclaredRole(name);
139 return outputs.computeIfAbsent(name, key -> objects.fileCollection());
99 140 }
100 141
101 public void declareOutputs(String name, String... extra) {
102 declaredOutputs.add(Objects.requireNonNull(name, "declareOutputs: The output name cannot be null"));
142 /**
143 * Declares allowed output roles. Roles must be declared before registering
144 * files under them.
145 */
146 public void declareRoles(String name, String... extra) {
147 declaredRoles.add(Objects.requireNonNull(name, "declareRoles: The output name cannot be null"));
103 148 for (var x : extra)
104 declaredOutputs.add(Objects.requireNonNull(x, "declareOutputs: The output name cannot be null"));
149 declaredRoles.add(Objects.requireNonNull(x, "declareRoles: The output name cannot be null"));
105 150 }
106 151
107 @Override
108 public GenericSourceSet configure(@SuppressWarnings("rawtypes") Closure configure) {
109 Closures.apply(configure, this);
110 return this;
152 /**
153 * Alias for {@link #declareRoles(String, String...)} kept for DSL clarity.
154 */
155 public void declareOutputs(String name, String... extra) {
156 declareRoles(name, extra);
157 }
158
159 /**
160 * Registers files produced elsewhere under the given role.
161 */
162 public void registerOutput(String name, Object... files) {
163 output(name).from(files);
111 164 }
112 165
113 private void registerOutputOnDemand(String outputName) {
114 logger.info("SourceSet '{}': registerOutputOnDemand '{}'", name, outputName);
166 /**
167 * Registers output files produced by a task, using a mapper to extract the
168 * output from the task. The task will be added as a build dependency of this
169 * output.
170 */
171 public <T extends Task> void registerOutput(String name, TaskProvider<T> task,
172 Function<? super T, ?> mapper) {
173 output(name).from(task.map(mapper::apply))
174 .builtBy(task);
175 }
115 176
116 if (declaredOutputs.contains(outputName))
117 outputs.register(outputName);
177 /**
178 * Applies a Groovy closure to this source set, enabling DSL-style
179 * configuration.
180 */
181 @Override
182 public GenericSourceSet configure(Closure configure) {
183 Closures.apply(configure, this);
184 return this;
118 185 }
119 186
120 187 private SourceDirectorySet createSourceDirectorySet(String name) {
121 188 return objects.sourceDirectorySet(name, name);
122 189 }
123 190
191 private void requireDeclaredRole(String roleName) {
192 if (!declaredRoles.contains(roleName)) {
193 throw new InvalidUserDataException(
194 "Output role '" + roleName + "' is not declared for source set '" + name + "'");
195 }
196 }
197
124 198 private Callable<List<? extends FileCollection>> outputsProvider() {
125 return () -> outputs.stream()
126 .map(GenericSourceSetOutput::getFileCollection)
127 .toList();
199 return () -> outputs.values().stream().toList();
128 200 }
129 201
130 202 private Callable<Set<File>> sourceDirectoriesProvider() {
131 203 return () -> sourceDirectorySets.stream()
132 204 .flatMap(x -> x.getSrcDirs().stream())
133 205 .collect(Collectors.toSet());
134 206 }
135 207
136 208 }
@@ -1,60 +1,59
1 package org.implab.gradle.common;
1 package org.implab.gradle.common.sources;
2 2
3 3 import org.gradle.api.Action;
4 4 import org.gradle.api.Plugin;
5 5 import org.gradle.api.Project;
6 6 import org.gradle.api.logging.Logger;
7 7 import org.gradle.api.logging.Logging;
8 8 import org.gradle.api.tasks.Copy;
9 9 import org.gradle.api.tasks.Delete;
10 10 import org.gradle.api.tasks.TaskContainer;
11 11 import org.gradle.api.tasks.TaskProvider;
12 12 import org.gradle.language.base.plugins.LifecycleBasePlugin;
13 import org.implab.gradle.common.files.GenericSourceSet;
14 import org.implab.gradle.common.utils.Strings;
13 import org.implab.gradle.common.core.lang.Strings;
15 14
16 15 /**
17 16 * This plugin creates a {@code sources} extension which is
18 17 * a container for {@link GenericSourceSet}.
19 18 *
20 19 */
21 20 public abstract class SourcesPlugin implements Plugin<Project> {
22 21 private final Logger logger = Logging.getLogger(SourcesPlugin.class);
23 22
24 23 private final String POLICY_NAME = "SourcesPolicy";
25 24
26 25 private final String EXTENSION_NAME = "sources";
27 26
28 27 @Override
29 28 public void apply(Project target) {
30 29 target.getPlugins().apply(LifecycleBasePlugin.class);
31 30
32 31 var sources = target.getObjects().domainObjectContainer(GenericSourceSet.class);
33 32 var tasks = target.getTasks();
34 33 var clean = tasks.named(LifecycleBasePlugin.CLEAN_TASK_NAME);
35 34
36 35 target.getExtensions().add(EXTENSION_NAME, sources);
37 36
38 37 sources.configureEach(configureSourceSet(tasks, clean));
39 38 }
40 39
41 40 private Action<GenericSourceSet> configureSourceSet(TaskContainer tasks, TaskProvider<?> clean) {
42 41 return sourceSet -> {
43 42 var name = sourceSet.getName();
44 43
45 44 logger.info("{}: Processing source set '{}'", POLICY_NAME, name);
46 45
47 46 var taskName = "clean" + Strings.capitalize(sourceSet.getName());
48 47
49 48 logger.info("{}: Register task '{}' of type [{}]", POLICY_NAME, taskName, Copy.class.getTypeName());
50 49
51 50 var task = tasks.register(taskName, Delete.class, self -> {
52 51 self.delete(sourceSet.getOutputsDir());
53 52 });
54 53
55 54 clean.configure(self -> {
56 55 self.dependsOn(task);
57 56 });
58 57 };
59 58 }
60 59 }
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now