Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ target/
!**/src/main/**/target/
!**/src/test/**/target/
deps/
classes/

### IntelliJ IDEA ###
.idea/
Expand Down
45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,7 @@ But you can also simply download and unzip the [release package](https://github.

See:

```shell
$ jpm --help
```
Usage: jpm [-hV] [COMMAND]
Simple command line tool for managing Maven artifacts
-h, --help Show this help message and exit.
Expand Down Expand Up @@ -217,12 +216,48 @@ Commands:
Example:
jpm path org.apache.httpcomponents:httpclient:4.5.14

do Executes an action command defined in the app.yml file. Actions
can use variable substitution for classpath.
exec Executes a shell command that can use special tokens to deal with
OS-specific quirks like paths. This means that commands can be
written in a somewhat platform independent way and will work on
Windows, Linux and MacOS.

Supported tokens and what they expand to:
{{deps}} : the classpath of all dependencies defined in the app.yml file
{/} : the OS' file path separator
{:} : the OS' class path separator
{~} : the user's home directory using the OS' class path format
{;} : the OS' command separator
{./file/path} : a path using the OS' path format (must start with './'!)
{./lib:./ext} : a class path using the OS' class path format (must start with './'!)
@[ ... ] : writes contents to a file and inserts @<path-to-file> instead

In actuality the command is pretty smart and will try to do the
right thing, as long as {{deps}} is the only token you use. In
the examples below the first line shows how to do it the hard
way, by specifying everything manually, while the second line
shows how much easier it is when you can rely on the built-in
smart feature. Is the smart feature bothering you? Just use any
of the other tokens besides {{deps}} and it will be turned off.
By default args files will only be considered for Java commands
that are know to support them (java, javac, javadoc, etc), but
you can indicate that your command supports it as well by
adding a single @ as the first character of the command.

Example:
jpm exec javac -cp @[{{deps}}] -d {./out/classes} --source-path {./src/main/java} App.java
jpm exec javac -cp {{deps}} -d out/classes --source-path src/main/java App.java
jpm exec @kotlinc -cp {{deps}} -d out/classes src/main/kotlin/App.kt

do Executes an action command defined in the app.yml file. The
command is executed using the same rules as the exec command,
so it can use all the same tokens and features. You can also
pass additional arguments to the action using -a or --arg
followed by the argument value. You can chain multiple actions
and their arguments in a single command line.
Example:
jpm do build
jpm do test
jpm do test --arg verbose
jpm do build -a --fresh test -a verbose

clean Executes the 'clean' action as defined in the app.yml file.
build Executes the 'build' action as defined in the app.yml file.
Expand Down
2 changes: 2 additions & 0 deletions app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ actions:
build: ".{/}mvnw spotless:apply package -DskipTests"
run: "{./target/binary/bin/jpm}"
test: ".{/}mvnw test"
buildj: "javac -cp {{deps}} -d classes --source-path src/main/java src/main/java/org/codejive/jpm/Main.java"
runj: "java -cp classes:{{deps}} org.codejive.jpm.Main"
5 changes: 5 additions & 0 deletions misc/grammar.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
commands ::= <elem>+
elem ::= <group> | <separator> | <command>
separator ::= ';' | '&&' | '||'
group ::= '(' <commands> ')'
command ::= <word>+
63 changes: 46 additions & 17 deletions src/main/java/org/codejive/jpm/Jpm.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import org.codejive.jpm.config.AppInfo;
import org.codejive.jpm.util.*;
import org.eclipse.aether.artifact.Artifact;
Expand Down Expand Up @@ -85,11 +86,11 @@ public Jpm build() {
*
* @param artifactNames The artifacts to copy.
* @param sync Whether to sync the target directory or not.
* @return An instance of {@link SyncStats} containing the statistics of the copy operation.
* @return An instance of {@link SyncResult} containing the statistics of the copy operation.
* @throws IOException If an error occurred during the copy operation.
* @throws DependencyResolutionException If an error occurred during the dependency resolution.
*/
public SyncStats copy(String[] artifactNames, boolean sync)
public SyncResult copy(String[] artifactNames, boolean sync)
throws IOException, DependencyResolutionException {
return copy(artifactNames, Collections.emptyMap(), sync);
}
Expand All @@ -100,11 +101,11 @@ public SyncStats copy(String[] artifactNames, boolean sync)
* @param artifactNames The artifacts to copy.
* @param repos A map of additional repository names to URLs where artifacts can be found.
* @param sync Whether to sync the target directory or not.
* @return An instance of {@link SyncStats} containing the statistics of the copy operation.
* @return An instance of {@link SyncResult} containing the statistics of the copy operation.
* @throws IOException If an error occurred during the copy operation.
* @throws DependencyResolutionException If an error occurred during the dependency resolution.
*/
public SyncStats copy(String[] artifactNames, Map<String, String> repos, boolean sync)
public SyncResult copy(String[] artifactNames, Map<String, String> repos, boolean sync)
throws IOException, DependencyResolutionException {
List<Path> files = Resolver.create(artifactNames, repos).resolvePaths();
return FileUtils.syncArtifacts(files, directory, noLinks, !sync);
Expand Down Expand Up @@ -140,11 +141,11 @@ private static String artifactGav(Artifact artifact) {
* basically means sync-copying the artifacts to the target directory.
*
* @param artifactNames The artifacts to install.
* @return An instance of {@link SyncStats} containing the statistics of the install operation.
* @return An instance of {@link SyncResult} containing the statistics of the install operation.
* @throws IOException If an error occurred during the install operation.
* @throws DependencyResolutionException If an error occurred during the dependency resolution.
*/
public SyncStats install(String[] artifactNames)
public SyncResult install(String[] artifactNames)
throws IOException, DependencyResolutionException {
return install(artifactNames, Collections.emptyMap());
}
Expand All @@ -157,18 +158,18 @@ public SyncStats install(String[] artifactNames)
*
* @param artifactNames The artifacts to install.
* @param extraRepos A map of additional repository names to URLs where artifacts can be found.
* @return An instance of {@link SyncStats} containing the statistics of the install operation.
* @return An instance of {@link SyncResult} containing the statistics of the install operation.
* @throws IOException If an error occurred during the install operation.
* @throws DependencyResolutionException If an error occurred during the dependency resolution.
*/
public SyncStats install(String[] artifactNames, Map<String, String> extraRepos)
public SyncResult install(String[] artifactNames, Map<String, String> extraRepos)
throws IOException, DependencyResolutionException {
AppInfo appInfo = AppInfo.read();
String[] artifacts = getArtifacts(artifactNames, appInfo);
Map<String, String> repos = getRepositories(extraRepos, appInfo);
if (artifacts.length > 0) {
List<Path> files = Resolver.create(artifacts, repos).resolvePaths();
SyncStats stats = FileUtils.syncArtifacts(files, directory, noLinks, true);
SyncResult stats = FileUtils.syncArtifacts(files, directory, noLinks, true);
if (artifactNames.length > 0) {
for (String dep : artifactNames) {
int p = dep.lastIndexOf(':');
Expand All @@ -181,7 +182,7 @@ public SyncStats install(String[] artifactNames, Map<String, String> extraRepos)
}
return stats;
} else {
return new SyncStats();
return new SyncResult();
}
}

Expand Down Expand Up @@ -215,7 +216,13 @@ public List<Path> path(String[] artifactNames, Map<String, String> extraRepos)
String[] deps = getArtifacts(artifactNames, appInfo);
Map<String, String> repos = getRepositories(extraRepos, appInfo);
if (deps.length > 0) {
return Resolver.create(deps, repos).resolvePaths();
List<Path> files = Resolver.create(deps, repos).resolvePaths();
if (artifactNames.length > 0) {
return files;
} else {
SyncResult result = FileUtils.syncArtifacts(files, directory, noLinks, true);
return result.files;
}
} else {
return Collections.emptyList();
}
Expand All @@ -240,7 +247,7 @@ private Map<String, String> getRepositories(Map<String, String> extraRepos, AppI
/**
* Executes an action defined in app.yml file.
*
* @param actionName The name of the action to execute (null to list actions)
* @param actionName The name of the action to execute
* @return An integer containing the exit result of the action
* @throws IllegalArgumentException If the action name is not provided or not found
* @throws IOException If an error occurred during the operation
Expand All @@ -260,13 +267,15 @@ public int executeAction(String actionName, List<String> args)
+ "' not found in app.yml. Use --list to see available actions.");
}

// Get the classpath for variable substitution only if needed
List<Path> classpath = Collections.emptyList();
if (command.contains("{{deps}}")) {
classpath = this.path(new String[0]); // Empty array means use dependencies from app.yml
// Add the user arguments to the command
if (args != null && !args.isEmpty()) {
command +=
args.stream()
.map(ScriptUtils::quoteArgument)
.collect(Collectors.joining(" ", " ", ""));
}

return ScriptUtils.executeScript(command, args, classpath, verbose);
return executeCommand(command);
}

/**
Expand All @@ -279,4 +288,24 @@ public List<String> listActions() throws IOException {
AppInfo appInfo = AppInfo.read();
return new ArrayList<>(appInfo.getActionNames());
}

/**
* Executes an action defined in app.yml file.
*
* @param command The command to execute
* @return An integer containing the exit result of the action
* @throws IOException If an error occurred during the operation
* @throws DependencyResolutionException If an error occurred during dependency resolution
* @throws InterruptedException If the action execution was interrupted
*/
public int executeCommand(String command)
throws IOException, DependencyResolutionException, InterruptedException {
// Get the classpath for variable substitution only if needed
List<Path> classpath = Collections.emptyList();
if (command.contains("{{deps}}")) {
classpath = this.path(new String[0]); // Empty array means use dependencies from app.yml
}

return ScriptUtils.executeScript(command, classpath, verbose);
}
}
77 changes: 67 additions & 10 deletions src/main/java/org/codejive/jpm/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
//DEPS org.yaml:snakeyaml:2.4
//DEPS org.jline:jline-console-ui:3.30.5 org.jline:jline-terminal-jni:3.30.5
//DEPS org.slf4j:slf4j-api:2.0.17 org.slf4j:slf4j-simple:2.0.17
//SOURCES Jpm.java config/AppInfo.java util/FileUtils.java util/Resolver.java util/ScriptUtils.java
//SOURCES util/SearchResult.java util/SearchUtils.java util/SyncStats.java util/Version.java
//SOURCES Jpm.java config/AppInfo.java util/CommandsParser.java util/FileUtils.java util/Resolver.java
//SOURCES util/ScriptUtils.java util/SearchResult.java util/SearchUtils.java util/SyncResult.java util/Version.java
// spotless:on

package org.codejive.jpm;
Expand All @@ -17,7 +17,7 @@
import java.util.*;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import org.codejive.jpm.util.SyncStats;
import org.codejive.jpm.util.SyncResult;
import org.codejive.jpm.util.Version;
import org.jline.consoleui.elements.InputValue;
import org.jline.consoleui.elements.ListChoice;
Expand Down Expand Up @@ -49,6 +49,7 @@
Main.Search.class,
Main.Install.class,
Main.PrintPath.class,
Main.Exec.class,
Main.Do.class,
Main.Clean.class,
Main.Build.class,
Expand Down Expand Up @@ -77,7 +78,7 @@ static class Copy implements Callable<Integer> {

@Override
public Integer call() throws Exception {
SyncStats stats =
SyncResult stats =
Jpm.builder()
.directory(artifactsMixin.depsMixin.directory)
.noLinks(artifactsMixin.depsMixin.noLinks)
Expand Down Expand Up @@ -141,7 +142,7 @@ public Integer call() throws Exception {
String selectedArtifact = getSelectedId(result, "item");
String artifactAction = getSelectedId(result, "action");
if ("install".equals(artifactAction)) {
SyncStats stats =
SyncResult stats =
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
Expand All @@ -151,7 +152,7 @@ public Integer call() throws Exception {
printStats(stats);
}
} else if ("copy".equals(artifactAction)) {
SyncStats stats =
SyncResult stats =
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
Expand Down Expand Up @@ -285,7 +286,7 @@ static class Install implements Callable<Integer> {

@Override
public Integer call() throws Exception {
SyncStats stats =
SyncResult stats =
Jpm.builder()
.directory(optionalArtifactsMixin.depsMixin.directory)
.noLinks(optionalArtifactsMixin.depsMixin.noLinks)
Expand Down Expand Up @@ -331,11 +332,67 @@ public Integer call() throws Exception {
}
}

@Command(
name = "exec",
description =
"Executes a shell command that can use special tokens to deal with OS-specific quirks like paths."
+ " This means that commands can be written in a somewhat platform independent way and will work on Windows, Linux and MacOS.\n"
+ "\n"
+ "Supported tokens and what they expand to:\n"
+ " {{deps}} : the classpath of all dependencies defined in the app.yml file\n"
+ " {/} : the OS' file path separator\n"
+ " {:} : the OS' class path separator\n"
+ " {~} : the user's home directory using the OS' class path format\n"
+ " {;} : the OS' command separator\n"
+ " {./file/path} : a path using the OS' path format (must start with './'!)\n"
+ " {./lib:./ext} : a class path using the OS' class path format (must start with './'!)\n"
+ " @[ ... ] : writes contents to a file and inserts @<path-to-file> instead\n"
+ "\n"
+ "In actuality the command is pretty smart and will try to do the right thing, as long as {{deps}} is the only token you use."
+ " In the examples below the first line shows how to do it the hard way, by specifying everything manually, while the second line shows how much easier it is when you can rely on the built-in smart feature."
+ " Is the smart feature bothering you? Just use any of the other tokens besides {{deps}} and it will be turned off."
+ " By default args files will only be considered for Java commands that are know to support them (java, javac, javadoc, etc), but you can indicate that your command supports it as well by adding a single @ as the first character of the command.\n"
+ "\n"
+ "Example:\n"
+ " jpm exec javac -cp @[{{deps}}] -d {./out/classes} --source-path {./src/main/java} App.java\n"
+ " jpm exec javac -cp {{deps}} -d out/classes --source-path src/main/java App.java\n"
+ " jpm exec @kotlinc -cp {{deps}} -d out/classes src/main/kotlin/App.kt\n")
static class Exec implements Callable<Integer> {
@Mixin DepsMixin depsMixin;
@Mixin QuietMixin quietMixin;

@Parameters(paramLabel = "command", description = "The command to execute", arity = "0..*")
private List<String> command;

@Override
public Integer call() throws Exception {
String cmd = String.join(" ", command);
try {
return Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.verbose(!quietMixin.quiet)
.build()
.executeCommand(cmd);
} catch (Exception e) {
System.err.println(e.getMessage());
return 1;
}
}
}

@Command(
name = "do",
description =
"Executes an action command defined in the app.yml file.\n\n"
+ "Example:\n jpm do build\n jpm do test --arg verbose\n")
"Executes an action command defined in the app.yml file."
+ " The command is executed using the same rules as the exec command, so it can use all the same tokens and features."
+ " You can also pass additional arguments to the action using -a or --arg followed by the argument value."
+ " You can chain multiple actions and their arguments in a single command line."
+ "\n"
+ "Example:\n"
+ " jpm do build\n"
+ " jpm do test --arg verbose\n"
+ " jpm do build -a --fresh test -a verbose\n")
static class Do implements Callable<Integer> {
@Mixin DepsMixin depsMixin;
@Mixin QuietMixin quietMixin;
Expand Down Expand Up @@ -570,7 +627,7 @@ static class QuietMixin {
private boolean quiet;
}

private static void printStats(SyncStats stats) {
private static void printStats(SyncResult stats) {
System.err.printf(
"Artifacts new: %d, updated: %d, deleted: %d%n",
(Integer) stats.copied, (Integer) stats.updated, (Integer) stats.deleted);
Expand Down
Loading
Loading