Skip to content

Commit fb47430

Browse files
maxandersenclaude
andauthored
feat: add WAR file support with full JAR parity (#2431)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 5e70414 commit fb47430

23 files changed

+467
-11
lines changed

docs/modules/ROOT/pages/dependencies.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ jbang eu.maveniverse.maven.plugins:toolbox:0.1.9:cli@fatjar
116116

117117
Without `@fatjar` it would resolve dependencies which would be redundant.
118118

119+
== Dependencies with WAR Files
120+
121+
You can add dependencies to WAR files just like JAR files:
122+
123+
[source, bash]
124+
----
125+
$ jbang --deps org.postgresql:postgresql:42.3.1 app.war
126+
----
127+
128+
WAR files support all dependency-related features including `//DEPS` directives, BOM POMs, and custom repositories.
129+
119130
== Managed dependencies ("BOM POM"'s) [Experimental]
120131

121132
When using libraries and frameworks it can get tedious to manage and update multiple versions.

docs/modules/ROOT/pages/running.adoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ ifdef::env-github[]
1212
:warning-caption: :warning:
1313
endif::[]
1414

15+
== Running WAR Files
16+
17+
JBang can run executable WAR files that contain a Main-Class manifest entry:
18+
19+
[source, bash]
20+
----
21+
$ jbang https://download.structurizr.com/structurizr-2026.03.06.war version
22+
----
23+
24+
WAR files are treated identically to JAR files and support all the same features:
25+
26+
* Run with arguments: `jbang app.war arg1 arg2`
27+
* Add dependencies: `jbang --deps com.google.gson:gson:2.8.9 app.war`
28+
* Use in interactive mode: `jbang --interactive app.war`
29+
* Create aliases and use in catalogs
30+
31+
The WAR file must contain a Main-Class entry in its META-INF/MANIFEST.MF, just like
32+
executable JAR files.
33+
1534
== Interactive REPL
1635

1736
`jbang --interactive` enables use of `jshell` to explore and use your script and any dependencies in a REPL editor.

docs/modules/cli/pages/jbang-run.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ jbang-run - Builds and runs provided script. (default command)
3939

4040
Builds and runs provided script. (default command)
4141

42+
The script can be a Java source file, JAR file, WAR file, or URL reference.
43+
WAR files are supported just like JAR files and must contain a Main-Class manifest entry.
44+
4245
// end::picocli-generated-man-section-description[]
4346

4447
// tag::picocli-generated-man-section-options[]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public Integer doCall() throws IOException {
167167
ProjectBuilder pb = createProjectBuilder();
168168
final Project prj = pb.build(scriptMixin.scriptOrFile);
169169

170-
if (prj.isJar() || prj.getMainSourceSet().getSources().isEmpty()) {
170+
if (prj.isExecutableArchive() || prj.getMainSourceSet().getSources().isEmpty()) {
171171
throw new ExitException(EXIT_INVALID_INPUT, "You can only edit source files");
172172
}
173173

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ int apply(BuildContext ctx) throws IOException {
555555
}
556556

557557
Project prj = ctx.getProject();
558-
if (prj.isJar() || prj.getMainSourceSet().getSources().isEmpty()) {
558+
if (prj.isExecutableArchive() || prj.getMainSourceSet().getSources().isEmpty()) {
559559
Util.errorMsg("You can only export source files");
560560
return EXIT_INVALID_INPUT;
561561
}

src/main/java/dev/jbang/resources/resolvers/RenamingScriptResourceResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public ResourceRef resolve(String resource) {
5757
List<String> knownExtensions = forceType != null ? Collections.singletonList(forceType.extension)
5858
: Source.Type.extensions();
5959
String ext = Util.extension(probe.getName());
60-
if (!ext.equals("jar")
60+
if (!ext.equals("jar") && !ext.equals("war")
6161
&& !knownExtensions.contains(ext)
6262
&& (!Util.isPreview() || !Project.BuildFile.fileNames().contains(probe.getName()))) {
6363
if (probe.isDirectory()) {

src/main/java/dev/jbang/source/AppBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ public CmdGeneratorBuilder build() throws IOException {
4949
// always build the jar for native mode
5050
// it allows integrations the options to produce the native image
5151
boolean buildRequired = true;
52-
if (project.isJar()) {
53-
Util.verboseMsg("The resource is a jar, no compilation to be done.");
52+
if (project.isExecutableArchive()) {
53+
Util.verboseMsg("The resource is an executable archive, no compilation to be done.");
5454
buildRequired = false;
5555
} else if (fresh) {
5656
Util.verboseMsg("Building as fresh build explicitly requested.");

src/main/java/dev/jbang/source/BuildContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public Project getProject() {
5757
public Path getJarFile() {
5858
if (project.isJShell()) {
5959
return null;
60-
} else if (project.isJar()) {
60+
} else if (project.isExecutableArchive()) {
6161
return project.getResourceRef().getFile();
6262
} else {
6363
return getBasePath(".jar");

src/main/java/dev/jbang/source/CodeBuilderProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected Builder<CmdGeneratorBuilder> getBuilder(BuildContext ctx) {
7676
if (prj.getMainSource() != null) {
7777
return prj.getMainSource().getBuilder(ctx);
7878
} else {
79-
if (prj.isJar() && prj.isNativeImage()) {
79+
if (prj.isExecutableArchive() && prj.isNativeImage()) {
8080
// JARs normally don't need building unless a native image
8181
// was requested
8282
return new JavaSource.JavaAppBuilder(ctx);

src/main/java/dev/jbang/source/Project.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,14 +322,38 @@ public static Builder<CmdGeneratorBuilder> codeBuilder(BuildContext ctx) {
322322
return CodeBuilderProvider.create(ctx).get();
323323
}
324324

325+
/**
326+
* @return true if this project is backed by an executable archive (jar or war)
327+
* @deprecated Use isExecutableArchive() for clarity. Kept for backwards
328+
* compatibility.
329+
*/
330+
@Deprecated
325331
public boolean isJar() {
326-
return Project.isJar(getResourceRef().getFile());
332+
return isExecutableArchive();
333+
}
334+
335+
public boolean isExecutableArchive() {
336+
return Project.isExecutableArchive(getResourceRef().getFile());
337+
}
338+
339+
static boolean isExecutableArchive(Path backingFile) {
340+
return hasExecutableExtension(backingFile);
327341
}
328342

329343
static boolean isJar(Path backingFile) {
330344
return backingFile != null && backingFile.toString().endsWith(".jar");
331345
}
332346

347+
/**
348+
* Checks if the given file has an executable archive extension (.jar or .war)
349+
*
350+
* @param backingFile the file to check
351+
* @return true if the file has a .jar or .war extension, false otherwise
352+
*/
353+
static boolean hasExecutableExtension(Path backingFile) {
354+
return Util.hasExecutableExtension(backingFile);
355+
}
356+
333357
public boolean isJShell() {
334358
return Project.isJShell(getResourceRef().getFile());
335359
}

0 commit comments

Comments
 (0)