Skip to content

Commit 160ee67

Browse files
authored
feat: jbang deps add <deps> <file> (jbangdev#2260)
1 parent 9756268 commit 160ee67

36 files changed

+2593
-13
lines changed

build.gradle

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ plugins {
2424

2525
def allureVersion = '2.29.1'
2626
def aspectJVersion = '1.9.25'
27+
def versionJline = '3.30.5'
2728

2829
configurations {
2930
agent {
@@ -166,13 +167,17 @@ dependencies {
166167
runtimeOnly "eu.maveniverse.maven.mima.runtime:standalone-static:2.4.36"
167168
implementation "org.apache.maven:maven-model:3.9.11"
168169

170+
implementation "org.jline:jline-console-ui:$versionJline"
171+
implementation "org.jline:jline-terminal-jni:$versionJline"
172+
169173
implementation "eu.maveniverse.maven.nisse:core:0.6.2"
170174
implementation "eu.maveniverse.maven.nisse.sources:os-source:0.6.2"
171175

172176
testImplementation platform('org.junit:junit-bom:5.14.1')
173177
testImplementation "org.junit.jupiter:junit-jupiter"
174178
testImplementation "org.junit.platform:junit-platform-launcher"
175179
testImplementation "com.github.stefanbirkner:system-rules:1.17.2"
180+
testImplementation "org.assertj:assertj-core:3.24.2"
176181
testImplementation "org.hamcrest:hamcrest-library:2.2"
177182
testImplementation "org.wiremock:wiremock:3.13.2"
178183
testImplementation platform("io.qameta.allure:allure-bom:$allureVersion")
@@ -324,15 +329,15 @@ compileJava9Java {
324329
}
325330

326331
shadowJar {
327-
minimize() {
332+
/*minimize() {
328333
//exclude(dependency('org.slf4j:slf4j-api:.*'))
329334
exclude(dependency('dev.jbang:devkitman:.*'))
330335
exclude(dependency('eu.maveniverse.maven.mima:context:.*'))
331336
exclude(dependency('eu.maveniverse.maven.mima.runtime:standalone-static:.*'))
332337
exclude(dependency('org.slf4j:jcl-over-slf4j:.*'))
333338
exclude(dependency('org.slf4j:slf4j-nop:.*'))
334339
exclude(dependency('org.jboss.logging:jboss-logging:.*'))
335-
}
340+
}*/
336341
mergeServiceFiles()
337342
manifest {
338343
attributes(

src/main/java/dev/jbang/Main.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static void main(String... args) {
2222
} catch (IOException e) {
2323
// Ignore
2424
}
25+
2526
CommandLine cli = JBang.getCommandLine();
2627
args = handleDefaultRun(cli.getCommandSpec(), args);
2728
int exitcode = cli.execute(args);

src/main/java/dev/jbang/cli/BaseCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public abstract class BaseCommand implements Callable<Integer> {
3030
public static final int EXIT_INTERNAL_ERROR = 4;
3131
public static final int EXIT_EXECUTE = 255;
3232

33-
private static final Logger logger = Logger.getLogger("org.jboss.shrinkwrap.resolver");
33+
private static final Logger logger = Logger.getLogger("dev.jbang.cli");
3434
static {
3535
logger.setLevel(Level.SEVERE);
3636
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package dev.jbang.cli;
2+
3+
import java.io.IOError;
4+
import java.io.IOException;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.nio.file.Paths;
8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.List;
11+
import java.util.Optional;
12+
13+
import org.eclipse.aether.artifact.Artifact;
14+
import org.jline.terminal.Terminal;
15+
import org.jline.terminal.TerminalBuilder;
16+
17+
import dev.jbang.dependencies.DependencyUtil;
18+
import dev.jbang.search.ArtifactSearchWidget;
19+
import dev.jbang.source.update.FileUpdateStrategy;
20+
import dev.jbang.source.update.FileUpdaters;
21+
22+
import picocli.CommandLine;
23+
import picocli.CommandLine.Command;
24+
25+
@Command(name = "deps", description = "Manage dependencies in jbang files.", subcommands = { DepsAdd.class,
26+
DepsSearch.class })
27+
public class Deps extends BaseCommand {
28+
29+
@Override
30+
public Integer doCall() throws IOException {
31+
// This is a parent command, subcommands handle the actual work
32+
return EXIT_OK;
33+
}
34+
}
35+
36+
@Command(name = "search", header = "Search for artifacts in local and central Maven repositories.", description = {
37+
"",
38+
" ${COMMAND-FULL-NAME}",
39+
" (to search for artifacts interactively)",
40+
" or ${COMMAND-FULL-NAME} jash",
41+
" (to search for 'jash' interactively)",
42+
" or ${COMMAND-FULL-NAME} myapp.java",
43+
" (to search for dependencies and add them to myapp.java)",
44+
" or ${COMMAND-FULL-NAME} jash myapp.java",
45+
" (to search initially for 'jash' and add dependency to myapp.java)",
46+
"",
47+
" note: JBang will detect if the arguments are a file or query, but if you want to be explicit you can use the --query and --target options.",
48+
"",
49+
"" })
50+
class DepsSearch extends BaseCommand {
51+
52+
@CommandLine.Option(names = "--max", description = "Maximum number of results to return.", defaultValue = "100")
53+
int max;
54+
55+
@CommandLine.Option(names = { "--query",
56+
"-q" }, description = "Artifact pattern to search for.", arity = "0..1")
57+
Optional<String> query;
58+
59+
@CommandLine.Option(names = {
60+
"--target" }, description = "Target where to add the dependency, i.e. app.java or build.jbang", arity = "0..1")
61+
Optional<Path> target;
62+
63+
@CommandLine.Parameters(arity = "0..2", paramLabel = "[query] [target]", description = "Artifact pattern to search for and target where to add the dependency, i.e. app.java or build.jbang. Query and target can both be optional")
64+
List<String> args = new ArrayList<>();
65+
66+
private boolean looksLikeATarget(String a0) {
67+
return Files.exists(Paths.get(a0));
68+
}
69+
70+
private void updateTarget(String a0) throws IOException {
71+
if (target.isPresent()) {
72+
throw new IllegalArgumentException("Cannot provide both target as as parameter and as option");
73+
} else {
74+
target = Optional.of(Paths.get(a0));
75+
}
76+
}
77+
78+
private void updateQuery(String a0) {
79+
if (query.isPresent()) {
80+
throw new IllegalArgumentException("Cannot provide both query as as parameter and as option");
81+
} else {
82+
query = Optional.of(a0);
83+
}
84+
}
85+
86+
@Override
87+
public Integer doCall() throws IOException {
88+
89+
if (args.size() == 1) { // either query or target
90+
String a0 = args.get(0);
91+
if (looksLikeATarget(a0)) {
92+
updateTarget(a0);
93+
} else {
94+
updateQuery(a0);
95+
}
96+
} else if (args.size() == 2) { // both query and target
97+
updateQuery(args.get(0));
98+
updateTarget(args.get(1));
99+
}
100+
101+
if (target.isPresent() && !Files.exists(target.get())) {
102+
throw new ExitException(EXIT_INVALID_INPUT, "Target file does not exist: " + target.get());
103+
}
104+
105+
try (Terminal terminal = TerminalBuilder.builder().system(true).build()) {
106+
try {
107+
Artifact artifact = new ArtifactSearchWidget(terminal).search(query.orElse(""));
108+
if (target.isPresent()) {
109+
DepsAdd.updateFile(target.get(), Collections.singletonList(artifactGav(artifact)));
110+
info("Added " + artifactGav(artifact) + " to " + target.get());
111+
} else {
112+
System.out.printf("%s:%s:%s%n", artifact.getGroupId(), artifact.getArtifactId(),
113+
artifact.getVersion());
114+
}
115+
return EXIT_OK;
116+
} catch (IOError e) {
117+
return EXIT_INVALID_INPUT;
118+
}
119+
}
120+
}
121+
122+
private static String artifactGav(Artifact artifact) {
123+
return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion();
124+
}
125+
126+
}
127+
128+
@Command(name = "add", description = "Add dependencies to a jbang file.")
129+
class DepsAdd extends BaseCommand {
130+
131+
@CommandLine.Parameters(description = "Dependencies to add (groupId:artifactId:version) and target file (.java or build.jbang)")
132+
List<String> parameters = new ArrayList<>();
133+
134+
@Override
135+
public Integer doCall() throws IOException {
136+
if (parameters.size() < 2) {
137+
throw new ExitException(EXIT_INVALID_INPUT, "At least one dependency and target file are required");
138+
}
139+
140+
// Last parameter is the target file
141+
Path targetFile = Paths.get(parameters.get(parameters.size() - 1));
142+
List<String> dependencies = parameters.subList(0, parameters.size() - 1);
143+
144+
if (!Files.exists(targetFile)) {
145+
throw new ExitException(EXIT_INVALID_INPUT, "Target file does not exist: " + targetFile);
146+
}
147+
148+
updateFile(targetFile, dependencies);
149+
150+
info("Added dependencies to " + targetFile);
151+
return EXIT_OK;
152+
}
153+
154+
static public void updateFile(Path file, List<String> dependencies) throws IOException {
155+
// Validate dependencies
156+
for (String dep : dependencies) {
157+
if (!DependencyUtil.looksLikeAGav(dep)) {
158+
throw new ExitException(EXIT_INVALID_INPUT, "Invalid dependency format: " + dep);
159+
}
160+
}
161+
if (!Files.exists(file)) {
162+
throw new ExitException(EXIT_INVALID_INPUT, "File does not exist: " + file);
163+
}
164+
165+
FileUpdateStrategy strategy = FileUpdaters.forFile(file);
166+
strategy.updateFile(file, dependencies);
167+
}
168+
}

src/main/java/dev/jbang/cli/JBang.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"" }, versionProvider = VersionProvider.class, subcommands = {
6161
Run.class, Build.class, Edit.class, Init.class, Alias.class, Template.class, Catalog.class, Trust.class,
6262
Cache.class, Completion.class, Jdk.class, Version.class, Wrapper.class, Info.class, App.class,
63-
Export.class, Config.class })
63+
Export.class, Config.class, Deps.class })
6464
public class JBang extends BaseCommand {
6565

6666
@CommandLine.Option(names = { "-V",
@@ -286,7 +286,7 @@ private Map<String, List<String>> sections() {
286286
if (sections == null) {
287287
sections = new LinkedHashMap<>();
288288
sections.put("Essentials", asList("run", "build"));
289-
sections.put("Editing", asList("init", "edit"));
289+
sections.put("Editing", asList("init", "edit", "deps"));
290290
sections.put("Caching", asList("cache", "export", "jdk"));
291291
sections.put("Configuration", asList("config", "trust", "alias", "template", "catalog", "app"));
292292
sections.put("Other", asList("completion", "info", "version", "wrapper"));
@@ -311,9 +311,15 @@ private Map<String, String> externals() {
311311
*
312312
* @param help
313313
*/
314-
public void validate(CommandLine.Help help) {
314+
public void validate(CommandLine.Help help, boolean ignoreExternalCommands) {
315315
Set<String> cmds = new HashSet<>();
316-
sections().forEach((key, value) -> cmds.addAll(value));
316+
sections().forEach((key, value) -> {
317+
if (ignoreExternalCommands && "External".equals(key)) {
318+
return;
319+
} else {
320+
cmds.addAll(value);
321+
}
322+
});
317323

318324
Set<String> actualcmds = new HashSet<>(help.subcommands().keySet());
319325

@@ -328,9 +334,6 @@ public void validate(CommandLine.Help help) {
328334
if (actualcmds.size() > 0) {
329335
throw new IllegalStateException(("Commands found with no assigned section" + actualcmds));
330336
}
331-
332-
sections().forEach((key, value) -> cmds.addAll(value));
333-
334337
}
335338

336339
@Override
@@ -426,6 +429,7 @@ private static Map<String, String> findExternalCommands() {
426429
Util.verboseMsg("Error trying to list aliases", ex);
427430
}
428431
// Now add any commands found on the PATH whose names start with "jbang-"
432+
// but only if they don't already exist (catalog aliases take precedence)
429433
try {
430434
List<Path> paths = getPluginPaths();
431435
List<Path> cmds = findCommandsWith(paths, p -> p.getFileName().toString().startsWith("jbang-"));
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package dev.jbang.search;
2+
3+
import java.io.IOException;
4+
import java.util.Collections;
5+
import java.util.List;
6+
7+
import org.eclipse.aether.artifact.Artifact;
8+
9+
public interface ArtifactSearch {
10+
11+
/**
12+
* Find artifacts matching the given pattern. This will return the first page of
13+
* results. If the pattern to search for is a simple name (there are no colons
14+
* in the string), the search will match any part of an artifact's group or
15+
* name. If there's a single colon, the search will match any part of the group
16+
* id and artifact id separately. If there are two colons, the search will match
17+
* the group id and artifact id exactly, and will return the artifact's
18+
* versions. If the pattern starts with "fc:", the search will match the full
19+
* class name. If the pattern starts with "c:", the search will match the class
20+
* name.
21+
*
22+
* @param artifactPattern The pattern to search for.
23+
* @param count The maximum number of results to return.
24+
* @return The search result as an instance of {@link SearchResult}.
25+
* @throws IOException If an error occurred during the search.
26+
*/
27+
SearchResult findArtifacts(String artifactPattern, int count) throws IOException;
28+
29+
/**
30+
* Find the next page of artifacts. This takes a {@link SearchResult} returned
31+
* by a previous call to {@link #findArtifacts(String, int)} and returns the
32+
* next page of results.
33+
*
34+
* @param prevResult The previous search result.
35+
* @return The next search result as an instance of {@link SearchResult}.
36+
* @throws IOException If an error occurred during the search.
37+
*/
38+
SearchResult findNextArtifacts(SearchResult prevResult) throws IOException;
39+
40+
enum Backends {
41+
rest_smo,
42+
rest_csc;
43+
// smo_smo,
44+
// smo_csc;
45+
}
46+
47+
static ArtifactSearch getBackend(Backends backend) {
48+
if (backend != null) {
49+
switch (backend) {
50+
case rest_smo:
51+
return SolrArtifactSearch.createSmo();
52+
case rest_csc:
53+
return SolrArtifactSearch.createCsc();
54+
// case smo_smo:
55+
// return SearchSmoApiImpl.createSmo();
56+
// case smo_csc:
57+
// return SearchSmoApiImpl.createCsc();
58+
}
59+
}
60+
return SolrArtifactSearch.createSmo();
61+
}
62+
63+
/**
64+
* Hold the result of a search while also functioning as a kind of bookmark for
65+
* paging purposes.
66+
*/
67+
class SearchResult {
68+
/** The artifacts that matched the search query. */
69+
public final List<? extends Artifact> artifacts;
70+
71+
/** The search query that produced this result. */
72+
public final String query;
73+
74+
/**
75+
* The index of the first artifact in this result relative to the total result
76+
* set.
77+
*/
78+
public final int start;
79+
80+
/** The maximum number of results to return */
81+
public final int count;
82+
83+
/** The total number of artifacts that matched the search query. */
84+
public final int total;
85+
86+
/**
87+
* Create a new search result.
88+
*
89+
* @param artifacts The artifacts that matched the search query.
90+
* @param query The search query that produced this result.
91+
* @param start The index of the first artifact in this result relative to
92+
* the total result set.
93+
* @param count The maximum number of results to return.
94+
* @param total The total number of artifacts that matched the search query.
95+
*/
96+
public SearchResult(
97+
List<? extends Artifact> artifacts, String query, int start, int count, int total) {
98+
this.artifacts = Collections.unmodifiableList(artifacts);
99+
100+
this.query = query;
101+
this.start = start;
102+
this.count = count;
103+
this.total = total;
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)