Skip to content

Commit affadad

Browse files
Copilotquintesse
authored andcommitted
feat: Implement action execution system
Actions can be added to the app.yml file. Each action is simply a command that will get executed when the given action is activated with `jpm do <action>`. Commands can contain special "variables" that will get replaced just before execution. The most important is `{{deps}}` which will result in the full classpath to all dependencies defined in the app.yml being inserted in the command. The remaining one all have to do with making it easier to create os-specific paths (so commands will work both on WIndows and Linux/Mac).
1 parent 43a8dcb commit affadad

File tree

13 files changed

+1305
-42
lines changed

13 files changed

+1305
-42
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Always manually validate jpm functionality after making changes:
8484

8585
### Important Files
8686
- `pom.xml` - Maven configuration with Spotless formatting, Shade plugin, Appassembler
87-
- `app.yml` - Example dependency configuration (also created by jpm install)
87+
- `app.yml` - jpm's actual runtime dependencies and actions (NOT an example file). Dependencies should be kept up-to-date with the (non-test) dependencies in pom.xml
8888
- `RELEASE.md` - Release process documentation
8989
- `.gitignore` - Excludes target/, deps/, IDE files
9090

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ on:
1818
jobs:
1919
build:
2020

21-
runs-on: ubuntu-latest
21+
runs-on: ${{ matrix.os }}
22+
strategy:
23+
matrix:
24+
os: [ubuntu-latest, windows-latest]
2225

2326
steps:
2427
- uses: actions/checkout@v5

README.md

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,76 @@ to get the required dependencies to run the code.
8989
_NB: We could have used `jpm copy` instead of `jpm install` to copy the dependencies but that would not have created
9090
the `app.yml` file._
9191
92+
## Actions
93+
94+
The `app.yml` file doesn't just track dependencies - it can also define custom actions that can be executed with the `jpm do` command or through convenient alias commands.
95+
96+
### Defining Actions
97+
98+
Actions are defined in the `actions` section of your `app.yml` file:
99+
100+
```yaml
101+
dependencies:
102+
com.github.lalyos:jfiglet:0.0.9
103+
104+
actions:
105+
build: "javac -cp {{deps}} *.java"
106+
run: "java -cp {{deps}} HelloWorld"
107+
test: "java -cp {{deps}} TestRunner"
108+
clean: "rm -f *.class"
109+
```
110+
111+
### Executing Actions
112+
113+
You can execute actions using the `jpm do` command:
114+
115+
```shell
116+
$ jpm do build
117+
$ jpm do run
118+
$ jpm do --list # Lists all available actions
119+
```
120+
121+
Or use the convenient alias commands:
122+
123+
```shell
124+
$ jpm build # Executes the 'build' action
125+
$ jpm run # Executes the 'run' action
126+
$ jpm test # Executes the 'test' action
127+
$ jpm clean # Executes the 'clean' action
128+
```
129+
130+
Alias commands can accept additional arguments that will be passed through to the underlying action:
131+
132+
```shell
133+
$ jpm run --verbose debug # Passes '--verbose debug' to the run action
134+
```
135+
136+
### Variable Substitution
137+
138+
Actions support several variable substitution features for cross-platform compatibility:
139+
140+
- **`{{deps}}`** - Replaced with the full classpath of all dependencies
141+
- **`{/}`** - Replaced with the file separator (`\` on Windows, `/` on Linux/Mac)
142+
- **`{:}`** - Replaced with the path separator (`;` on Windows, `:` on Linux/Mac)
143+
- **`{~}`** - Replaced with the user's home directory (The actual path on Windows, `~` on Linux/Mac)
144+
- **`{./path/to/file}`** - Converts relative paths to platform-specific format
145+
- **`{./libs:./ext:~/usrlibs}`** - Converts entire class paths to platform-specific format
146+
147+
Example with cross-platform compatibility:
148+
149+
```yaml
150+
actions:
151+
build: "javac -cp {{deps}} -d {./target/classes} src{/}*.java"
152+
run: "java -cp {{deps}}{:}{./target/classes} Main"
153+
test: "java -cp {{deps}}{:}{./target/classes} org.junit.runner.JUnitCore TestSuite"
154+
```
155+
156+
NB: The `{{deps}}` variable substitution is only performed when needed - if your action doesn't contain `{{deps}}`, jpm won't resolve the classpath, making execution faster for simple actions that don't require dependencies.
157+
158+
NB2: These actions are just a very simple convenience feature. For a much more full-featured cross-platform action runner I recommend taking a look at:
159+
160+
- [Just](https://github.com/casey/just) - Just a command runner
161+
92162
## Installation
93163
94164
For now the simplest way to install `jpm` is to use [JBang](https://www.jbang.dev/download/):
@@ -132,8 +202,8 @@ Commands:
132202
dependencies to the target directory while at the same time
133203
removing any artifacts that are no longer needed (ie the ones
134204
that are not mentioned in the app.yml file). If no artifacts
135-
are passed the app.yml file will be left untouched and only
136-
the existing dependencies in the file will be copied.
205+
are passed the app.yml file will be left untouched and only the
206+
existing dependencies in the file will be copied.
137207
138208
Example:
139209
jpm install org.apache.httpcomponents:httpclient:4.5.14
@@ -145,6 +215,18 @@ Commands:
145215
146216
Example:
147217
jpm path org.apache.httpcomponents:httpclient:4.5.14
218+
219+
do Executes an action command defined in the app.yml file. Actions
220+
can use variable substitution for classpath.
221+
222+
Example:
223+
jpm do build
224+
jpm do test
225+
226+
clean Executes the 'clean' action as defined in the app.yml file.
227+
build Executes the 'build' action as defined in the app.yml file.
228+
run Executes the 'run' action as defined in the app.yml file.
229+
test Executes the 'test' action as defined in the app.yml file.
148230
```
149231
150232
## Development

app.yml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
dependencies:
2-
eu.maveniverse.maven.mima:context: "2.4.15"
3-
eu.maveniverse.maven.mima.runtime:standalone-static: "2.4.15"
4-
info.picocli:picocli: "4.7.6"
5-
org.yaml:snakeyaml: "2.3"
6-
org.jline:jline-console-ui: "3.29.0"
7-
org.jline:jline-terminal-jni: "3.29.0"
8-
org.slf4j:slf4j-api: "2.0.13"
9-
org.slf4j:slf4j-log4j12: "2.0.13"
10-
org.slf4j:slf4j-simple: "2.0.13"
2+
eu.maveniverse.maven.mima:context: "2.4.33"
3+
eu.maveniverse.maven.mima.runtime:standalone-static: "2.4.33"
4+
info.picocli:picocli: "4.7.7"
5+
org.yaml:snakeyaml: "2.4"
6+
org.jline:jline-console-ui: "3.30.5"
7+
org.jline:jline-terminal-jni: "3.30.5"
8+
org.slf4j:slf4j-api: "2.0.17"
9+
org.slf4j:slf4j-simple: "2.0.17"
10+
11+
actions:
12+
clean: ".{/}mvnw clean"
13+
build: ".{/}mvnw spotless:apply package -DskipTests"
14+
run: "{./target/binary/bin/jpm}"
15+
test: ".{/}mvnw test"

pom.xml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
<version.slf4j>2.0.17</version.slf4j>
4040
<version.spotless>2.46.1</version.spotless>
4141
<version.google-java-format>1.22.0</version.google-java-format>
42+
<version.junit>5.11.4</version.junit>
43+
<version.mockito>5.15.2</version.mockito>
44+
<version.assertj>3.26.3</version.assertj>
4245
</properties>
4346

4447
<dependencies>
@@ -82,6 +85,32 @@
8285
<artifactId>slf4j-simple</artifactId>
8386
<version>${version.slf4j}</version>
8487
</dependency>
88+
89+
<!-- Test dependencies -->
90+
<dependency>
91+
<groupId>org.junit.jupiter</groupId>
92+
<artifactId>junit-jupiter</artifactId>
93+
<version>${version.junit}</version>
94+
<scope>test</scope>
95+
</dependency>
96+
<dependency>
97+
<groupId>org.mockito</groupId>
98+
<artifactId>mockito-core</artifactId>
99+
<version>${version.mockito}</version>
100+
<scope>test</scope>
101+
</dependency>
102+
<dependency>
103+
<groupId>org.mockito</groupId>
104+
<artifactId>mockito-junit-jupiter</artifactId>
105+
<version>${version.mockito}</version>
106+
<scope>test</scope>
107+
</dependency>
108+
<dependency>
109+
<groupId>org.assertj</groupId>
110+
<artifactId>assertj-core</artifactId>
111+
<version>${version.assertj}</version>
112+
<scope>test</scope>
113+
</dependency>
85114
</dependencies>
86115

87116
<build>
@@ -97,6 +126,11 @@
97126
<artifactId>maven-compiler-plugin</artifactId>
98127
<version>3.14.0</version>
99128
</plugin>
129+
<plugin>
130+
<groupId>org.apache.maven.plugins</groupId>
131+
<artifactId>maven-surefire-plugin</artifactId>
132+
<version>3.5.2</version>
133+
</plugin>
100134
<plugin>
101135
<groupId>org.apache.maven.plugins</groupId>
102136
<artifactId>maven-jar-plugin</artifactId>

src/main/java/org/codejive/jpm/Jpm.java

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
public class Jpm {
1313
private final Path directory;
1414
private final boolean noLinks;
15+
private final boolean verbose;
1516

16-
private Jpm(Path directory, boolean noLinks) {
17+
private Jpm(Path directory, boolean noLinks, boolean verbose) {
1718
this.directory = directory;
1819
this.noLinks = noLinks;
20+
this.verbose = verbose;
1921
}
2022

2123
/**
@@ -31,6 +33,7 @@ public static Builder builder() {
3133
public static class Builder {
3234
private Path directory;
3335
private boolean noLinks;
36+
private boolean verbose;
3437

3538
private Builder() {}
3639

@@ -56,13 +59,24 @@ public Builder noLinks(boolean noLinks) {
5659
return this;
5760
}
5861

62+
/**
63+
* Set whether to enable verbose output or not.
64+
*
65+
* @param verbose Whether to enable verbose output or not.
66+
* @return The builder instance for chaining.
67+
*/
68+
public Builder verbose(boolean verbose) {
69+
this.verbose = verbose;
70+
return this;
71+
}
72+
5973
/**
6074
* Builds the {@link Jpm} instance.
6175
*
6276
* @return A {@link Jpm} instance.
6377
*/
6478
public Jpm build() {
65-
return new Jpm(directory, noLinks);
79+
return new Jpm(directory, noLinks, verbose);
6680
}
6781
}
6882

@@ -167,6 +181,49 @@ private static String[] getArtifacts(String[] artifactNames, AppInfo appInfo) {
167181
return deps;
168182
}
169183

184+
/**
185+
* Executes an action defined in app.yml file.
186+
*
187+
* @param actionName The name of the action to execute (null to list actions)
188+
* @return An integer containing the exit result of the action
189+
* @throws IllegalArgumentException If the action name is not provided or not found
190+
* @throws IOException If an error occurred during the operation
191+
* @throws DependencyResolutionException If an error occurred during dependency resolution
192+
* @throws InterruptedException If the action execution was interrupted
193+
*/
194+
public int executeAction(String actionName, List<String> args)
195+
throws IOException, DependencyResolutionException, InterruptedException {
196+
AppInfo appInfo = AppInfo.read();
197+
198+
// Get the action command
199+
String command = appInfo.getAction(actionName);
200+
if (command == null) {
201+
throw new IllegalArgumentException(
202+
"Action '"
203+
+ actionName
204+
+ "' not found in app.yml. Use --list to see available actions.");
205+
}
206+
207+
// Get the classpath for variable substitution only if needed
208+
List<Path> classpath = Collections.emptyList();
209+
if (command.contains("{{deps}}")) {
210+
classpath = this.path(new String[0]); // Empty array means use dependencies from app.yml
211+
}
212+
213+
return ScriptUtils.executeScript(command, args, classpath, true);
214+
}
215+
216+
/**
217+
* Returns a list of available action names defined in the app.yml file.
218+
*
219+
* @return A list of available action names
220+
* @throws IOException If an error occurred during the operation
221+
*/
222+
public List<String> listActions() throws IOException {
223+
AppInfo appInfo = AppInfo.read();
224+
return new ArrayList<>(appInfo.getActionNames());
225+
}
226+
170227
private static boolean isWindows() {
171228
String os =
172229
System.getProperty("os.name")

0 commit comments

Comments
 (0)