diff --git a/docs/modules/ROOT/pages/troubleshooting.adoc b/docs/modules/ROOT/pages/troubleshooting.adoc index 0a50781e91..3a2959e8e3 100644 --- a/docs/modules/ROOT/pages/troubleshooting.adoc +++ b/docs/modules/ROOT/pages/troubleshooting.adoc @@ -149,6 +149,12 @@ jbang info classpath script.java # Test dependency resolution jbang info classpath script.java +# Resolve dependency source JARs (source-path) +jbang info source-path script.java + +# Resolve source JAR for a specific JAR or dependency +jbang info source-jar xyz.jar + # Check repositories jbang info docs script.java diff --git a/docs/modules/cli/nav.adoc b/docs/modules/cli/nav.adoc index 46b26f6180..0443af4780 100644 --- a/docs/modules/cli/nav.adoc +++ b/docs/modules/cli/nav.adoc @@ -57,6 +57,8 @@ *** xref:jbang:cli:jbang-info-tools.adoc[tools] *** xref:jbang:cli:jbang-info-classpath.adoc[classpath] *** xref:jbang:cli:jbang-info-jar.adoc[jar] +*** xref:jbang:cli:jbang-info-source-path.adoc[source-path] +*** xref:jbang:cli:jbang-info-source-jar.adoc[source-jar] *** xref:jbang:cli:jbang-info-docs.adoc[docs] ** xref:jbang:cli:jbang-version.adoc[version] ** xref:jbang:cli:jbang-wrapper.adoc[wrapper] diff --git a/docs/modules/cli/pages/jbang-info-source-jar.adoc b/docs/modules/cli/pages/jbang-info-source-jar.adoc new file mode 100644 index 0000000000..a42984f300 --- /dev/null +++ b/docs/modules/cli/pages/jbang-info-source-jar.adoc @@ -0,0 +1,85 @@ += JBANG-INFO-SOURCE-JAR + +== NAME + +jbang info source-jar -- Prints the path to this application's or artifact's source JAR file. + +== SYNOPSIS + +[source] +---- +jbang info source-jar [-h] [-s=] [--files=] [-T=] [--jsh] [--catalog=] [-D=] [--deps=] [--repos=] [--cp=] [--ignore-transitive-repositories] [--build-dir=] [--module] [--config=] [--insecure] [--[no-]verbose] [--[no-]quiet] [--[no-]offline] [--[no-]fresh] [--[no-]stacktrace] [] +---- + +== OPTIONS + +*-h*, *--help*:: +Display this help and exit + +*-s*, *--sources*=:: +Add additional sources. + +*--files*=:: +Add additional files. + +*-T*, *--source-type*=:: +Force input to be interpreted as the given type. Can be: java, jshell, groovy, kotlin, or markdown + +*--jsh*:: +Force input to be interpreted with jsh/jshell. Deprecated: use '--source-type jshell' + +*--catalog*=:: +Path to catalog file to be used instead of the default + +*-D*==:: +set a system property ++ +Default: `true` + +*--deps*=:: +Add additional dependencies (Use commas to separate them). + +*--repos*=:: +Add additional repositories. + +*--cp*=:: +Add class path entries. ++ +Aliases: `--class-path` + +*--ignore-transitive-repositories*:: +Ignore remote repositories found in transitive dependencies ++ +Aliases: `--itr` + +*--build-dir*=:: +Use given directory for build results + +*--module*=:: +Treat resource as a module. Optionally with the given module name + +*--config*=:: +Path to config file to be used instead of the default + +*--insecure*:: +Enable insecure trust of all SSL certificates. + +*--[no-]verbose*:: +jbang will be verbose on what it does. + +*--[no-]quiet*:: +jbang will be quiet, only print when error occurs. + +*-o*, *--[no-]offline*:: +Work offline. Fail-fast if dependencies are missing. + +*--[no-]fresh*:: +Make sure we use fresh (i.e. non-cached) resources. + +*-x*, *--[no-]stacktrace*:: +Print exceptions stacktraces to stderr (even when quiet). + +== ARGUMENTS + +**:: +A file or URL to a Java code file diff --git a/docs/modules/cli/pages/jbang-info-source-path.adoc b/docs/modules/cli/pages/jbang-info-source-path.adoc new file mode 100644 index 0000000000..65d6c3ed59 --- /dev/null +++ b/docs/modules/cli/pages/jbang-info-source-path.adoc @@ -0,0 +1,88 @@ += JBANG-INFO-SOURCE-PATH + +== NAME + +jbang info source-path -- Prints source-path used for this application using operating system specific path separation. + +== SYNOPSIS + +[source] +---- +jbang info source-path [-h] [--deps-only] [-s=] [--files=] [-T=] [--jsh] [--catalog=] [-D=] [--deps=] [--repos=] [--cp=] [--ignore-transitive-repositories] [--build-dir=] [--module] [--config=] [--insecure] [--[no-]verbose] [--[no-]quiet] [--[no-]offline] [--[no-]fresh] [--[no-]stacktrace] [] +---- + +== OPTIONS + +*-h*, *--help*:: +Display this help and exit + +*--deps-only*:: +Only include the dependencies in the output, not the application jar itself + +*-s*, *--sources*=:: +Add additional sources. + +*--files*=:: +Add additional files. + +*-T*, *--source-type*=:: +Force input to be interpreted as the given type. Can be: java, jshell, groovy, kotlin, or markdown + +*--jsh*:: +Force input to be interpreted with jsh/jshell. Deprecated: use '--source-type jshell' + +*--catalog*=:: +Path to catalog file to be used instead of the default + +*-D*==:: +set a system property ++ +Default: `true` + +*--deps*=:: +Add additional dependencies (Use commas to separate them). + +*--repos*=:: +Add additional repositories. + +*--cp*=:: +Add class path entries. ++ +Aliases: `--class-path` + +*--ignore-transitive-repositories*:: +Ignore remote repositories found in transitive dependencies ++ +Aliases: `--itr` + +*--build-dir*=:: +Use given directory for build results + +*--module*=:: +Treat resource as a module. Optionally with the given module name + +*--config*=:: +Path to config file to be used instead of the default + +*--insecure*:: +Enable insecure trust of all SSL certificates. + +*--[no-]verbose*:: +jbang will be verbose on what it does. + +*--[no-]quiet*:: +jbang will be quiet, only print when error occurs. + +*-o*, *--[no-]offline*:: +Work offline. Fail-fast if dependencies are missing. + +*--[no-]fresh*:: +Make sure we use fresh (i.e. non-cached) resources. + +*-x*, *--[no-]stacktrace*:: +Print exceptions stacktraces to stderr (even when quiet). + +== ARGUMENTS + +**:: +A file or URL to a Java code file diff --git a/docs/modules/cli/pages/jbang-info.adoc b/docs/modules/cli/pages/jbang-info.adoc index c3db56ce2d..d67b580dae 100644 --- a/docs/modules/cli/pages/jbang-info.adoc +++ b/docs/modules/cli/pages/jbang-info.adoc @@ -42,5 +42,7 @@ Print exceptions stacktraces to stderr (even when quiet). * xref:jbang:cli:jbang-info-tools.adoc[*tools*] - Prints a json description usable for tools/IDE's to get classpath and more info for a jbang script/application. * xref:jbang:cli:jbang-info-classpath.adoc[*classpath*] - Prints class-path used for this application using operating system specific path separation. * xref:jbang:cli:jbang-info-jar.adoc[*jar*] - Prints the path to this application's JAR file. +* xref:jbang:cli:jbang-info-source-path.adoc[*source-path*] - Prints source-path used for this application using operating system specific path separation. +* xref:jbang:cli:jbang-info-source-jar.adoc[*source-jar*] - Prints the path to this application's or artifact's source JAR file. * xref:jbang:cli:jbang-info-docs.adoc[*docs*] - Open the documentation file in the default browser. diff --git a/docs/modules/cli/partials/nav-jbang.adoc b/docs/modules/cli/partials/nav-jbang.adoc index 93e28df659..eff02105e5 100644 --- a/docs/modules/cli/partials/nav-jbang.adoc +++ b/docs/modules/cli/partials/nav-jbang.adoc @@ -38,6 +38,8 @@ *** xref:jbang:cli:jbang-info-tools.adoc[tools] *** xref:jbang:cli:jbang-info-classpath.adoc[classpath] *** xref:jbang:cli:jbang-info-jar.adoc[jar] +*** xref:jbang:cli:jbang-info-source-path.adoc[source-path] +*** xref:jbang:cli:jbang-info-source-jar.adoc[source-jar] *** xref:jbang:cli:jbang-info-docs.adoc[docs] ** xref:jbang:cli:jbang-app.adoc[app] *** xref:jbang:cli:jbang-app-install.adoc[install] diff --git a/src/main/java/dev/jbang/cli/Info.java b/src/main/java/dev/jbang/cli/Info.java index a0ea78ec9e..0eb09e82e8 100644 --- a/src/main/java/dev/jbang/cli/Info.java +++ b/src/main/java/dev/jbang/cli/Info.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -30,6 +31,7 @@ import dev.jbang.ExitException; import dev.jbang.dependencies.ArtifactInfo; +import dev.jbang.dependencies.DependencyUtil; import dev.jbang.dependencies.MavenRepo; import dev.jbang.devkitman.Jdk; import dev.jbang.devkitman.JdkManager; @@ -47,6 +49,7 @@ @CommandDefinition(name = "info", description = "Provides info about the script for tools (and humans who are tools).", groupCommands = { Info.Tools.class, Info.ClassPath.class, Info.Jar.class, + Info.SourcePath.class, Info.SourceJar.class, Info.Docs.class }, generateHelp = true) public class Info extends BaseCommand { @@ -109,12 +112,14 @@ static class ScriptInfo { String originalResource; String backingResource; String applicationJar; + String applicationSourceJar; String applicationJsa; String nativeImage; String mainClass; List dependencies; List repositories; List resolvedDependencies; + List resolvedSourceDependencies; String javaVersion; String requestedJavaVersion; String availableJdkPath; @@ -128,6 +133,10 @@ static class ScriptInfo { Map> docs; public ScriptInfo(Project prj, Path buildDir, boolean assureJdkInstalled) { + this(prj, buildDir, assureJdkInstalled, false); + } + + public ScriptInfo(Project prj, Path buildDir, boolean assureJdkInstalled, boolean downloadSources) { originalResource = prj.getResourceRef().getOriginalResource(); if (scripts.add(originalResource)) { @@ -137,7 +146,7 @@ public ScriptInfo(Project prj, Path buildDir, boolean assureJdkInstalled) { try { BuildContext ctx = BuildContext.forProject(prj, buildDir); - init(ctx); + init(ctx, downloadSources); } catch (Exception e) { Util.warnMsg("Unable to obtain full information, the script probably contains errors", e); } @@ -207,7 +216,7 @@ private void init(SourceSet ss) { } } - private void init(BuildContext ctx) { + private void init(BuildContext ctx, boolean downloadSources) { applicationJar = ctx.getJarFile() == null ? null : ctx.getJarFile().toAbsolutePath().toString(); applicationJsa = ctx.getJsaFile() != null && Files.isRegularFile(ctx.getJsaFile()) @@ -220,11 +229,19 @@ private void init(BuildContext ctx) { List artifacts = ctx.resolveClassPath().getArtifacts(); if (artifacts.isEmpty()) { resolvedDependencies = Collections.emptyList(); + resolvedSourceDependencies = Collections.emptyList(); } else { resolvedDependencies = artifacts .stream() .map(a -> a.getFile().toString()) .collect(Collectors.toList()); + resolvedSourceDependencies = artifacts + .stream() + .map(ArtifactInfo::getSourceFile) + .filter(Objects::nonNull) + .filter(Files::exists) + .map(Path::toString) + .collect(Collectors.toList()); } if (ctx.getJarFile() != null && Files.exists(ctx.getJarFile())) { @@ -233,6 +250,55 @@ private void init(BuildContext ctx) { gav = jarProject.getGav().orElse(gav); module = ModuleUtil.getModuleName(jarProject); } + + if (downloadSources) { + resolveApplicationSourceJar(ctx); + } + } + + private void resolveApplicationSourceJar(BuildContext ctx) { + Project prj = ctx.getProject(); + if (!prj.isExecutableArchive()) { + return; + } + Path jar = ctx.getJarFile(); + if (jar == null || !Files.exists(jar)) { + return; + } + + String fileName = jar.getFileName().toString(); + if (fileName.endsWith(".jar")) { + Path sibling = jar.resolveSibling(fileName.substring(0, fileName.length() - 4) + "-sources.jar"); + if (Files.isRegularFile(sibling)) { + applicationSourceJar = sibling.toAbsolutePath().toString(); + return; + } + } + + List artifacts = ctx.resolveClassPath().getArtifacts(); + for (ArtifactInfo art : artifacts) { + if (jar.equals(art.getFile())) { + if (art.getSourceFile() != null && Files.exists(art.getSourceFile())) { + applicationSourceJar = art.getSourceFile().toAbsolutePath().toString(); + return; + } else if (art.isSourcesChecked()) { + return; + } + break; + } + } + + String targetGav = gav != null ? gav : prj.getGav().orElse(null); + if (targetGav == null && prj.getResourceRef().getOriginalResource() != null + && DependencyUtil.looksLikeAGav(prj.getResourceRef().getOriginalResource())) { + targetGav = prj.getResourceRef().getOriginalResource(); + } + if (targetGav != null) { + Optional srcJar = DependencyUtil.resolveSource(targetGav, prj.getRepositories()); + if (srcJar.isPresent() && Files.exists(srcJar.get())) { + applicationSourceJar = srcJar.get().toAbsolutePath().toString(); + } + } } /** @@ -260,15 +326,21 @@ Map> getDocsMap(List docs) { private static Set scripts; ScriptInfo getInfo(boolean assureJdkInstalled) { - scriptMixin.validate(); + return getInfo(assureJdkInstalled, false); + } + ScriptInfo getInfo(boolean assureJdkInstalled, boolean downloadSources) { + scriptMixin.validate(); + if (downloadSources) { + Util.setDownloadSources(true); + } ProjectBuilder pb = createProjectBuilder(); Project prj = pb.build(scriptMixin.scriptOrFile); scripts = new HashSet<>(); Path bd = buildDir != null ? Paths.get(buildDir) : null; - return new ScriptInfo(prj, bd, assureJdkInstalled); + return new ScriptInfo(prj, bd, assureJdkInstalled, downloadSources || Util.downloadSources()); } ProjectBuilder createProjectBuilder() { @@ -293,11 +365,17 @@ public static class Tools extends BaseInfoCommand { @Option(name = "select", description = "Indicate the name of the field to select and return from the full info result") String select; + @Option(name = "download-sources", hasValue = false, description = "Resolve and include source JARs in the output") + boolean downloadSources; + @Override public Integer doCall() throws IOException { Gson parser = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); - ScriptInfo info = getInfo(true); + boolean needSources = downloadSources + || "applicationSourceJar".equals(select) + || "resolvedSourceDependencies".equals(select); + ScriptInfo info = getInfo(true, needSources); if (select != null) { try { Field f = info.getClass().getDeclaredField(select); @@ -362,6 +440,48 @@ public Integer doCall() throws IOException { } } + @CommandDefinition(name = "source-path", aliases = { "sourcepath", + "sources-path" }, description = "Prints source-path used for this application using operating system specific path separation.", generateHelp = true) + public static class SourcePath extends BaseInfoCommand { + + @Option(name = "deps-only", hasValue = false, description = "Only include the dependencies in the output, not the application jar itself") + boolean dependenciesOnly; + + @Override + public Integer doCall() throws IOException { + + ScriptInfo info = getInfo(false, true); + List deps = info.resolvedSourceDependencies != null ? info.resolvedSourceDependencies + : Collections.emptyList(); + List sp = new ArrayList<>(deps.size() + 1); + if (!dependenciesOnly && info.applicationSourceJar != null + && !deps.contains(info.applicationSourceJar)) { + sp.add(info.applicationSourceJar); + } + sp.addAll(deps); + out.println(String.join(CP_SEPARATOR, sp)); + + return ExitException.EXIT_OK; + } + } + + @CommandDefinition(name = "source-jar", aliases = { "sourcejar", + "sources-jar" }, description = "Prints the path to this application's or artifact's source JAR file.", generateHelp = true) + public static class SourceJar extends BaseInfoCommand { + + @Override + public Integer doCall() throws IOException { + ScriptInfo info = getInfo(false, true); + if (info.applicationSourceJar != null) { + out.println(info.applicationSourceJar); + return ExitException.EXIT_OK; + } else { + throw new ExitException(ExitException.EXIT_GENERIC_ERROR, + "No source JAR found for: " + scriptMixin.scriptOrFile); + } + } + } + @CommandDefinition(name = "docs", description = "Open the documentation file in the default browser.", generateHelp = true) public static class Docs extends BaseInfoCommand { diff --git a/src/main/java/dev/jbang/dependencies/ArtifactInfo.java b/src/main/java/dev/jbang/dependencies/ArtifactInfo.java index 1813216c79..59dbe84b7b 100644 --- a/src/main/java/dev/jbang/dependencies/ArtifactInfo.java +++ b/src/main/java/dev/jbang/dependencies/ArtifactInfo.java @@ -4,6 +4,8 @@ import java.nio.file.Path; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import dev.jbang.util.ModuleUtil; /** @@ -13,17 +15,32 @@ public class ArtifactInfo { private final MavenCoordinate coordinate; private final Path file; + private final @Nullable Path sourceFile; + private final boolean sourcesChecked; private final long timestamp; ArtifactInfo(MavenCoordinate coordinate, Path file) { + this(coordinate, file, null, false); + } + + ArtifactInfo(MavenCoordinate coordinate, Path file, @Nullable Path sourceFile, boolean sourcesChecked) { this.coordinate = coordinate; this.file = file; + this.sourceFile = sourceFile; + this.sourcesChecked = sourcesChecked; this.timestamp = Files.exists(file) ? file.toFile().lastModified() : 0; } ArtifactInfo(MavenCoordinate coordinate, Path file, long cachedTimestamp) { + this(coordinate, file, null, false, cachedTimestamp); + } + + ArtifactInfo(MavenCoordinate coordinate, Path file, @Nullable Path sourceFile, boolean sourcesChecked, + long cachedTimestamp) { this.coordinate = coordinate; this.file = file; + this.sourceFile = sourceFile; + this.sourcesChecked = sourcesChecked; this.timestamp = cachedTimestamp; } @@ -35,6 +52,14 @@ public Path getFile() { return file; } + public @Nullable Path getSourceFile() { + return sourceFile; + } + + public boolean isSourcesChecked() { + return sourcesChecked; + } + public long getTimestamp() { return timestamp; } diff --git a/src/main/java/dev/jbang/dependencies/ArtifactResolver.java b/src/main/java/dev/jbang/dependencies/ArtifactResolver.java index be9914079e..72db415625 100644 --- a/src/main/java/dev/jbang/dependencies/ArtifactResolver.java +++ b/src/main/java/dev/jbang/dependencies/ArtifactResolver.java @@ -183,9 +183,9 @@ public void close() { context.close(); } - public void downloadSources(Artifact artifact) { + public Optional downloadSources(Artifact artifact) { try { - context.repositorySystem() + ArtifactResult result = context.repositorySystem() .resolveArtifact(context.repositorySystemSession(), new ArtifactRequest() .setArtifact( new SubArtifact( @@ -194,9 +194,21 @@ public void downloadSources(Artifact artifact) { "jar")) .setRepositories( context.remoteRepositories())); + if (result.getArtifact() != null && result.getArtifact().getPath() != null) { + return Optional.of(result.getArtifact().getPath()); + } } catch (ArtifactResolutionException e) { Util.verboseMsg("Could not resolve sources for " + artifact); } + return Optional.empty(); + } + + public Optional resolveSource(MavenCoordinate coord) { + return downloadSources(toArtifact(coord)); + } + + public Optional resolveSource(String coord) { + return downloadSources(toArtifact(coord)); } public List resolve(List depIds) { @@ -245,12 +257,12 @@ public List resolve(List depIds) { return artifacts.stream() .map(ar -> { + Path sourcePath = null; if (downloadSources) { - downloadSources(ar.getArtifact()); + sourcePath = downloadSources(ar.getArtifact()).orElse(null); } - return ar.getArtifact(); + return toArtifactInfo(ar.getArtifact(), sourcePath, downloadSources); }) - .map(ArtifactResolver::toArtifactInfo) .collect(Collectors.toList()); } catch (DependencyResolutionException ex) { throw new ExitException(1, "Could not resolve dependencies: " + ex.getMessage(), ex); @@ -437,10 +449,14 @@ private Artifact toArtifact(MavenCoordinate coord) { return new DefaultArtifact(coord.getGroupId(), coord.getArtifactId(), cls, ext, coord.getVersion()); } - private static ArtifactInfo toArtifactInfo(Artifact artifact) { + private static ArtifactInfo toArtifactInfo(Artifact artifact, Path sourcePath, boolean sourcesChecked) { MavenCoordinate coord = new MavenCoordinate(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), artifact.getClassifier(), artifact.getExtension()); - return new ArtifactInfo(coord, artifact.getPath()); + return new ArtifactInfo(coord, artifact.getPath(), sourcePath, sourcesChecked); + } + + private static ArtifactInfo toArtifactInfo(Artifact artifact) { + return toArtifactInfo(artifact, null, false); } public static Path getLocalMavenRepo() { diff --git a/src/main/java/dev/jbang/dependencies/DependencyCache.java b/src/main/java/dev/jbang/dependencies/DependencyCache.java index 6e9687e388..bbe5094ac0 100644 --- a/src/main/java/dev/jbang/dependencies/DependencyCache.java +++ b/src/main/java/dev/jbang/dependencies/DependencyCache.java @@ -37,7 +37,14 @@ private static Map> getCache() { MavenCoordinate gav = MavenCoordinate.fromCanonicalString(jsonObject.get("gav").getAsString()); Path file = Paths.get(jsonObject.get("file").getAsString()); long ts = jsonObject.has("ts") ? jsonObject.get("ts").getAsLong() : 0; - return new ArtifactInfo(gav, file, ts); + Path sourceFile = null; + if (jsonObject.has("sources-file") && !jsonObject.get("sources-file").isJsonNull()) { + sourceFile = Paths.get(jsonObject.get("sources-file").getAsString()); + } + boolean sourcesChecked = (sourceFile != null) + || (jsonObject.has("sources-checked") + && jsonObject.get("sources-checked").getAsBoolean()); + return new ArtifactInfo(gav, file, sourceFile, sourcesChecked, ts); }; Gson parser = new GsonBuilder() .setPrettyPrinting() @@ -87,6 +94,12 @@ public static void cache(String depsHash, List artifacts) { JsonObject json = new JsonObject(); json.addProperty("gav", src.getCoordinate().toCanonicalForm()); json.addProperty("file", src.getFile().toString()); + if (src.getSourceFile() != null) { + json.addProperty("sources-file", src.getSourceFile().toString()); + } + if (src.isSourcesChecked()) { + json.addProperty("sources-checked", true); + } json.addProperty("ts", src.getTimestamp()); return json; }; diff --git a/src/main/java/dev/jbang/dependencies/DependencyUtil.java b/src/main/java/dev/jbang/dependencies/DependencyUtil.java index c3405e4b65..579f6cfb06 100644 --- a/src/main/java/dev/jbang/dependencies/DependencyUtil.java +++ b/src/main/java/dev/jbang/dependencies/DependencyUtil.java @@ -7,6 +7,7 @@ import static dev.jbang.util.Util.isWindows; import static dev.jbang.util.Util.verboseMsg; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -93,9 +94,14 @@ public static ModularClassPath resolveDependencies(List deps, List ai.isSourcesChecked() + && (ai.getSourceFile() == null || Files.exists(ai.getSourceFile()))); + if (sourcesValid) { + ModularClassPath mcp = new ModularClassPath(cachedDeps); + verboseMsg(String.format("Resolved artifact(s) from cache: %s", mcp)); + return mcp; + } } } @@ -220,4 +226,26 @@ private static boolean isRelativePathRepoReference(String reporef) { // use / return isWindows() && (reporef.startsWith(".\\") || reporef.startsWith("..\\")); } + + public static Optional resolveSource(String coord, List repos) { + return resolveSource(MavenCoordinate.fromString(coord), repos); + } + + public static Optional resolveSource(MavenCoordinate coord, List repos) { + List actualRepos = repos != null && !repos.isEmpty() ? repos + : Collections.singletonList(toMavenRepo("central")); + try (ArtifactResolver resolver = ArtifactResolver.Builder + .create() + .repositories(actualRepos) + .withUserSettings(true) + .localFolder(getJBangLocalMavenRepoOverride()) + .offline(dev.jbang.util.Util.isOffline()) + .ignoreTransitiveRepositories(dev.jbang.util.Util.isIgnoreTransitiveRepositories()) + .forceCacheUpdate(dev.jbang.util.Util.isFresh()) + .logging(!dev.jbang.util.Util.isQuiet()) + .downloadSources(true) + .build()) { + return resolver.resolveSource(coord); + } + } } diff --git a/src/main/java/dev/jbang/dependencies/ModularClassPath.java b/src/main/java/dev/jbang/dependencies/ModularClassPath.java index 9adcbda7e0..fa7def0a35 100644 --- a/src/main/java/dev/jbang/dependencies/ModularClassPath.java +++ b/src/main/java/dev/jbang/dependencies/ModularClassPath.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -32,6 +33,7 @@ public class ModularClassPath { private final List artifacts; private List classPaths; + private List sourcePaths; private Optional javafx = Optional.empty(); public ModularClassPath(List artifacts) { @@ -53,6 +55,24 @@ public String getClassPath() { return String.join(CP_SEPARATOR, getClassPaths()); } + public List getSourcePaths() { + if (sourcePaths == null) { + sourcePaths = artifacts + .stream() + .map(ArtifactInfo::getSourceFile) + .filter(Objects::nonNull) + .filter(Files::exists) + .map(it -> it.toAbsolutePath().toString()) + .distinct() + .collect(Collectors.toList()); + } + return sourcePaths; + } + + public String getSourcePath() { + return String.join(CP_SEPARATOR, getSourcePaths()); + } + public String getManifestPath() { return artifacts.stream() .map(it -> it.getFile().toAbsolutePath().toUri()) diff --git a/src/test/java/dev/jbang/cli/TestInfo.java b/src/test/java/dev/jbang/cli/TestInfo.java index 448955d140..af1d7204f5 100644 --- a/src/test/java/dev/jbang/cli/TestInfo.java +++ b/src/test/java/dev/jbang/cli/TestInfo.java @@ -11,9 +11,11 @@ import java.util.Collection; import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import dev.jbang.BaseTest; +import dev.jbang.ExitException; import dev.jbang.util.WarTestFixtures; public class TestInfo extends BaseTest { @@ -201,4 +203,113 @@ void testInfoClasspathForWar() throws IOException { Files.deleteIfExists(warPath); } } + + @Test + void testInfoSourceJarSibling() throws IOException { + Path testJar = cwdDir.resolve("mylib.jar"); + Path testSrcJar = cwdDir.resolve("mylib-sources.jar"); + Files.write(testJar, "dummy jar".getBytes()); + Files.write(testSrcJar, "dummy sources".getBytes()); + + Info.SourceJar sourceJar = JBang.parseCommand("info", "source-jar", testJar.toString()); + Info.BaseInfoCommand.ScriptInfo info = sourceJar.getInfo(false, true); + assertThat(info.applicationSourceJar, equalTo(testSrcJar.toAbsolutePath().toString())); + } + + @Test + void testInfoSourcePathSibling() throws IOException { + Path testJar = cwdDir.resolve("mylib2.jar"); + Path testSrcJar = cwdDir.resolve("mylib2-sources.jar"); + Files.write(testJar, "dummy jar".getBytes()); + Files.write(testSrcJar, "dummy sources".getBytes()); + + Info.SourcePath sourcePath = JBang.parseCommand("info", "source-path", testJar.toString()); + Info.BaseInfoCommand.ScriptInfo info = sourcePath.getInfo(false, true); + assertThat(info.applicationSourceJar, equalTo(testSrcJar.toAbsolutePath().toString())); + } + + @Test + void testInfoSourceJarNotFound() { + Path testJar = cwdDir.resolve("nosrc.jar"); + try { + Files.write(testJar, "dummy jar".getBytes()); + Info.SourceJar sourceJar = JBang.parseCommand("info", "source-jar", testJar.toString()); + Assertions.assertThrows(ExitException.class, sourceJar::doCall); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + void testInfoSourcePathWithDeps() { + String src = examplesTestFolder.resolve("quote.java").toString(); + Info.SourcePath sourcePath = JBang.parseCommand("info", "source-path", src); + Info.BaseInfoCommand.ScriptInfo info = sourcePath.getInfo(false, true); + assertThat(info.resolvedSourceDependencies, Matchers.>allOf( + hasSize(equalTo(1)), + everyItem(allOf(containsString("picocli"), endsWith("-sources.jar"))))); + } + + @Test + void testInfoSourceJarForGav() { + Info.SourceJar sourceJar = JBang.parseCommand("info", "source-jar", "info.picocli:picocli:4.6.3"); + Info.BaseInfoCommand.ScriptInfo info = sourceJar.getInfo(false, true); + assertThat(info.applicationSourceJar, allOf(containsString("picocli"), endsWith("-sources.jar"))); + } + + @Test + void testInfoToolsWithDownloadSources() { + String src = examplesTestFolder.resolve("quote.java").toString(); + Info.Tools tools = JBang.parseCommand("info", "tools", "--download-sources", src); + Info.BaseInfoCommand.ScriptInfo info = tools.getInfo(false, true); + assertThat(info.resolvedSourceDependencies, Matchers.>allOf( + hasSize(equalTo(1)), + everyItem(allOf(containsString("picocli"), endsWith("-sources.jar"))))); + } + + @Test + void testInfoSourcePathExecution() throws Exception { + String src = examplesTestFolder.resolve("quote.java").toString(); + CaptureResult result = checkedRun("info", "source-path", src); + assertThat(result.result, equalTo(0)); + assertThat(result.normalizedOut(), allOf(containsString("picocli"), containsString("-sources.jar"))); + } + + @Test + void testInfoSourceJarExecution() throws Exception { + CaptureResult result = checkedRun("info", "source-jar", "info.picocli:picocli:4.6.3"); + assertThat(result.result, equalTo(0)); + assertThat(result.normalizedOut(), allOf(containsString("picocli"), containsString("-sources.jar"))); + } + + @Test + void testInfoToolsSelectSourcesDoesNotRequireSourceJars() throws Exception { + String src = examplesTestFolder.resolve("quote.java").toString(); + CaptureResult result = checkedRun("info", "tools", "--select", "sources", src); + assertThat(result.result, equalTo(0)); + assertThat(result.normalizedOut(), containsString("quote.java")); + } + + @Test + void testInfoToolsSelectResolvedSourceDependencies() throws Exception { + String src = examplesTestFolder.resolve("quote.java").toString(); + CaptureResult result = checkedRun("info", "tools", "--select", "resolvedSourceDependencies", src); + assertThat(result.result, equalTo(0)); + assertThat(result.normalizedOut(), allOf(containsString("picocli"), containsString("-sources.jar"))); + } + + @Test + void testInfoSourceJarForGavWithoutSources() { + Info.SourceJar sourceJar = JBang.parseCommand("info", "source-jar", + "io.netty:netty-tcnative-boringssl-static:2.0.61.Final"); + Assertions.assertThrows(ExitException.class, sourceJar::doCall); + } + + @Test + void testInfoToolsForGavWithoutSources() { + Info.Tools tools = JBang.parseCommand("info", "tools", "--download-sources", + "io.netty:netty-tcnative-boringssl-static:2.0.61.Final"); + Info.BaseInfoCommand.ScriptInfo info = tools.getInfo(false, true); + assertThat(info.applicationSourceJar, is(nullValue())); + } } diff --git a/src/test/java/dev/jbang/dependencies/TestArtifactInfo.java b/src/test/java/dev/jbang/dependencies/TestArtifactInfo.java index d6e2b0e768..322d76a744 100644 --- a/src/test/java/dev/jbang/dependencies/TestArtifactInfo.java +++ b/src/test/java/dev/jbang/dependencies/TestArtifactInfo.java @@ -4,6 +4,9 @@ import static org.hamcrest.Matchers.*; import static org.hamcrest.io.FileMatchers.aFileWithSize; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -13,6 +16,7 @@ import dev.jbang.BaseTest; import dev.jbang.Settings; +import dev.jbang.util.Util; public class TestArtifactInfo extends BaseTest { @@ -39,4 +43,36 @@ public void testDependencyCache() { assertThat(wonka, contains(classpath.getArtifacts().toArray())); } + + @Test + public void testDependencyCacheWithSources() throws IOException { + DependencyCache.clear(); + + Path jar1 = Files.createFile(jbangTempDir.resolve("test.jar")); + Path src1 = Files.createFile(jbangTempDir.resolve("test-sources.jar")); + Path jar2 = Files.createFile(jbangTempDir.resolve("nosrc.jar")); + + MavenCoordinate coord = new MavenCoordinate("org.example", "test", "1.0", null, "jar"); + ArtifactInfo ai1 = new ArtifactInfo(coord, jar1, src1, true); + ArtifactInfo ai2 = new ArtifactInfo(new MavenCoordinate("org.example", "nosrc", "1.0", null, "jar"), + jar2, null, true); + + DependencyCache.cache("sources-test", Arrays.asList(ai1, ai2)); + + String content = Util.readFileContent(Settings.getCacheDependencyFile()); + assertThat(content, containsString("\"sources-file\":")); + assertThat(content, containsString("test-sources.jar")); + assertThat(content, containsString("\"sources-checked\": true")); + assertThat(content, not(containsString("sourceFile"))); + assertThat(content, not(containsString("sourcesChecked"))); + + DependencyCache.clear(); + List cached = DependencyCache.findDependenciesByHash("sources-test"); + assertThat(cached, notNullValue()); + assertThat(cached, hasSize(2)); + assertThat(cached.get(0).getSourceFile(), equalTo(src1)); + assertThat(cached.get(0).isSourcesChecked(), equalTo(true)); + assertThat(cached.get(1).getSourceFile(), is(nullValue())); + assertThat(cached.get(1).isSourcesChecked(), equalTo(true)); + } }