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
21 changes: 21 additions & 0 deletions cli/src/main/java/ca/weblite/jdeploy/packaging/PackageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.FileHeader;
import org.apache.commons.io.FileUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.w3c.dom.Document;

Expand Down Expand Up @@ -1058,6 +1059,9 @@ private void bundleJdeploy(PackagingContext context) throws IOException {
private String processJdeployTemplate(PackagingContext context, String jdeployContents) {
jdeployContents = jdeployContents.replace("{{JAVA_VERSION}}", String.valueOf(context.getJavaVersion(DEFAULT_JAVA_VERSION)));
jdeployContents = jdeployContents.replace("{{PORT}}", String.valueOf(context.getPort(0)));
// Inject the "jdeploy.args" array as a JSON literal so the launcher can
// apply publisher-declared JVM/program args (e.g. --add-opens) at runtime.
jdeployContents = jdeployContents.replace("{{JAVA_ARGS}}", toJavaArgsJson(context.getList("args", true)));
if (context.getWar(null) != null) {
jdeployContents = jdeployContents.replace("{{WAR_PATH}}", new File(context.getWar(null)).getName());
} else {
Expand Down Expand Up @@ -1085,6 +1089,23 @@ private String processJdeployTemplate(PackagingContext context, String jdeployCo
return jdeployContents;
}

/**
* Serializes the "jdeploy.args" list into a JSON array literal for injection
* into the bundled jdeploy.js launcher. Non-string entries are coerced to
* their string form; a null or empty list yields "[]".
*/
private String toJavaArgsJson(List args) {
JSONArray out = new JSONArray();
if (args != null) {
for (Object arg : args) {
if (arg != null) {
out.put(String.valueOf(arg));
}
}
}
return out.toString();
}

private void bundleJetty(PackagingContext context) throws IOException {
// Now we need to create the stub.
File bin = context.getJdeployBundleDir();
Expand Down
114 changes: 114 additions & 0 deletions cli/src/main/resources/ca/weblite/jdeploy/jdeploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ var mainClass = "{{MAIN_CLASS}}";
var classPath = "{{CLASSPATH}}";
var port = "{{PORT}}";
var warPath = "{{WAR_PATH}}";
// JVM/program arguments declared in the "jdeploy.args" array of package.json.
// Injected at publish time as a JSON array literal (defaults to []). These are
// processed at runtime (see processPackageArg/appendPackageArgs) so that
// platform-conditional args such as "-[mac]--add-opens ..." resolve on the
// machine the app actually runs on, mirroring the client4j launcher.
var packageArgs = {{JAVA_ARGS}};
var javaVersionString = "{{JAVA_VERSION}}";
var tryJavaHomeFirst = false;
var javafx = false;
Expand Down Expand Up @@ -584,6 +590,112 @@ if (!done) {



// Resolves the platform-conditional prefix syntax used in package.json
// "jdeploy.args" entries, mirroring processArg() in the client4j launcher.
//
// Supported prefixes (the bracket lists a comma-separated set of conditions
// that must ALL be satisfied; a condition may itself be a pipe-separated OR
// group such as "mac|linux"):
// -[conditions]<arg> generic, e.g. "-[mac]--add-opens java.desktop/...=..."
// -D[conditions]<rest> becomes "-D<rest>" when the conditions pass
// -X[conditions]<rest> becomes "-X<rest>" when the conditions pass
//
// Returns the resolved argument string, or '' if the conditions exclude the
// current platform (in which case the caller drops the argument).
function processPackageArg(arg) {
var isMac = (process.platform === 'darwin');
var isWin = (process.platform === 'win32');
var isLinux = (process.platform === 'linux');

if ((arg.indexOf('-D[') === 0 || arg.indexOf('-X[') === 0 || arg.indexOf('-[') === 0) && arg.indexOf(']') >= 0) {
var conditionsStr = arg.substring(arg.indexOf('[') + 1, arg.indexOf(']'));
var conditions = conditionsStr.split(',');
var fulfilled = true;
for (var i = 0; i < conditions.length; i++) {
var condition = conditions[i].trim();
var lcCondition = condition.toLowerCase();
if (lcCondition === 'mac' && !isMac) {
fulfilled = false;
continue;
} else if (lcCondition === 'win' && !isWin) {
fulfilled = false;
continue;
} else if (lcCondition === 'linux' && !isLinux) {
fulfilled = false;
continue;
} else if (lcCondition === 'windows' && !isWin) {
fulfilled = false;
continue;
}

if (condition.indexOf('|') >= 0) {
lcCondition = '|' + lcCondition + '|';
fulfilled = false;
if (isMac && lcCondition.indexOf('|mac|') >= 0) {
fulfilled = true;
} else if (isLinux && lcCondition.indexOf('|linux|') >= 0) {
fulfilled = true;
} else if (isWin && lcCondition.indexOf('|win|') >= 0) {
fulfilled = true;
} else if (isWin && lcCondition.indexOf('|windows|') >= 0) {
fulfilled = true;
}
}
}

if (!fulfilled) {
return '';
}
if (arg.indexOf('-[') === 0) {
arg = arg.substring(arg.indexOf(']') + 1);
} else if (arg.indexOf('-D[') === 0) {
arg = '-D' + arg.substring(arg.indexOf(']') + 1);
} else if (arg.indexOf('-X[') === 0) {
arg = '-X' + arg.substring(arg.indexOf(']') + 1);
}
}

return arg;
}

// Categorizes each package.json "jdeploy.args" entry into JVM args (placed
// before -jar) or program args (placed after the jar), mirroring the relevant
// part of processRunArgs() in the client4j launcher. A "--flag value" JVM
// option (e.g. "--add-opens java.desktop/com.apple.eawt=ALL-UNNAMED") is split
// into two tokens because the JVM expects the flag and value as separate
// arguments. Package args are appended before the user-supplied CLI args.
function appendPackageArgs(rawArgs, javaArgs, programArgs) {
if (!Array.isArray(rawArgs)) {
return;
}
rawArgs.forEach(function(rawArg) {
var arg = processPackageArg(rawArg);
if (arg === '' || arg === null || typeof arg === 'undefined') {
return;
}
if (arg.indexOf('-D') === 0 || arg.indexOf('-X') === 0) {
javaArgs.push(arg);
} else if (arg.indexOf('--') === 0) {
// JVM module/access options (--add-opens, --add-exports,
// --add-modules, --module-path, --enable-preview, ...). Split off a
// value if one is present in the same token.
var spaceIdx = arg.indexOf(' ');
if (spaceIdx >= 0) {
javaArgs.push(arg.substring(0, spaceIdx));
javaArgs.push(arg.substring(spaceIdx + 1));
} else {
javaArgs.push(arg);
}
} else if (arg.indexOf('-p ') === 0) {
// Short form of --module-path.
javaArgs.push('-p');
javaArgs.push(arg.substring(3));
} else {
programArgs.push(arg);
}
});
}

function run(_javaHome) {
var fail = reason => {
console.error(reason);
Expand All @@ -608,6 +720,8 @@ function run(_javaHome) {
javaArgs.push('-Djdeploy.port='+port);
javaArgs.push('-Djdeploy.war.path='+warPath);
var programArgs = [];
// Args declared in package.json (jdeploy.args) come before the user's CLI args.
appendPackageArgs(packageArgs, javaArgs, programArgs);
userArgs.forEach(function(arg) {
if (arg.startsWith('-D') || arg.startsWith('-X')) {
javaArgs.push(arg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ void testJDeployScript() throws Exception {
.replace("{{MAIN_CLASS}}", JAVA_CLASS)
.replace("{{JAVA_VERSION}}", "17") // Using JDK 17
.replace("{{JAVAFX}}", "false")
.replace("{{JDK}}", "false");
.replace("{{JDK}}", "false")
.replace("{{JAVA_ARGS}}", "[]"); // No package.json jdeploy.args in this test
System.out.println("DEBUG: Placeholders replaced successfully");

// Step 3: Save the modified script to a temp file inside jdeploy-bundle
Expand Down
204 changes: 204 additions & 0 deletions tests/jdeploy-js/package-args.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/usr/bin/env node
/*
* Regression test for package.json "jdeploy.args" support in the generated
* launcher (cli/.../jdeploy.js).
*
* The launcher must honour JVM/program args declared in package.json's
* jdeploy.args array, including the platform-conditional prefix syntax
* ("-[mac]...", "-D[win]...", "-X[linux]...") and the "--flag value" splitting
* that the JVM requires for options like --add-opens. This mirrors the
* processArg()/processRunArgs() behaviour of the client4j launcher.
*
* The test exercises the REAL processPackageArg/appendPackageArgs functions
* lifted straight out of jdeploy.js (by brace matching) so it tracks the
* shipped implementation rather than a copy of it. process.platform is
* temporarily overridden to exercise each platform branch on any host.
*/

'use strict';

const fs = require('fs');
const path = require('path');
const assert = require('assert');

const JDEPLOY_JS = path.resolve(
__dirname,
'..',
'..',
'cli',
'src',
'main',
'resources',
'ca',
'weblite',
'jdeploy',
'jdeploy.js'
);

function extractFunctionSource(source, name) {
const marker = 'function ' + name;
const start = source.indexOf(marker);
if (start < 0) {
throw new Error('Could not find `' + marker + '` in ' + JDEPLOY_JS);
}
let depth = 0;
let started = false;
for (let i = source.indexOf('{', start); i < source.length; i++) {
const ch = source[i];
if (ch === '{') {
depth++;
started = true;
} else if (ch === '}') {
depth--;
if (started && depth === 0) {
return source.slice(start, i + 1);
}
}
}
throw new Error('Unbalanced braces while extracting `' + name + '`');
}

function loadArgFns() {
const source = fs.readFileSync(JDEPLOY_JS, 'utf8');
const body =
extractFunctionSource(source, 'processPackageArg') +
'\n' +
extractFunctionSource(source, 'appendPackageArgs') +
'\nreturn { processPackageArg: processPackageArg, appendPackageArgs: appendPackageArgs };';
// Both functions only depend on the global `process` and `Array`.
return new Function(body)();
}

// Runs `fn` with process.platform pinned to `platform`, then restores it.
function withPlatform(platform, fn) {
const original = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
try {
return fn();
} finally {
Object.defineProperty(process, 'platform', original);
}
}

function categorize(fns, rawArgs, platform) {
return withPlatform(platform, function () {
const javaArgs = [];
const programArgs = [];
fns.appendPackageArgs(rawArgs, javaArgs, programArgs);
return { javaArgs: javaArgs, programArgs: programArgs };
});
}

function main() {
const fns = loadArgFns();

// --- The motivating case: mac-only --add-opens is split into two JVM args ---
const macAddOpens = ['-[mac]--add-opens java.desktop/com.apple.eawt=ALL-UNNAMED'];

assert.deepStrictEqual(
categorize(fns, macAddOpens, 'darwin'),
{
javaArgs: ['--add-opens', 'java.desktop/com.apple.eawt=ALL-UNNAMED'],
programArgs: [],
},
'mac --add-opens should be split into flag + value on macOS'
);

assert.deepStrictEqual(
categorize(fns, macAddOpens, 'win32'),
{ javaArgs: [], programArgs: [] },
'mac-only arg should be dropped on Windows'
);
assert.deepStrictEqual(
categorize(fns, macAddOpens, 'linux'),
{ javaArgs: [], programArgs: [] },
'mac-only arg should be dropped on Linux'
);

// --- Unconditional --add-opens is always split, on every platform ---
const bareAddOpens = ['--add-opens java.base/java.lang=ALL-UNNAMED'];
['darwin', 'win32', 'linux'].forEach(function (p) {
assert.deepStrictEqual(
categorize(fns, bareAddOpens, p),
{
javaArgs: ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'],
programArgs: [],
},
'unconditional --add-opens should be split on ' + p
);
});

// --- -D[win] / -X[linux] prefixes rewrite to -D.../-X... when they apply ---
assert.deepStrictEqual(
categorize(fns, ['-D[win]foo=bar'], 'win32'),
{ javaArgs: ['-Dfoo=bar'], programArgs: [] },
'-D[win] should become -Dfoo=bar on Windows'
);
assert.deepStrictEqual(
categorize(fns, ['-D[win]foo=bar'], 'linux'),
{ javaArgs: [], programArgs: [] },
'-D[win] should be dropped off Windows'
);
assert.deepStrictEqual(
categorize(fns, ['-X[linux]mx512m'], 'linux'),
{ javaArgs: ['-Xmx512m'], programArgs: [] },
'-X[linux]mx512m should become -Xmx512m on Linux'
);

// --- Pipe-OR conditions: mac|linux applies on both, not Windows ---
const orArg = ['-[mac|linux]--add-exports java.base/sun.nio.ch=ALL-UNNAMED'];
assert.deepStrictEqual(
categorize(fns, orArg, 'darwin').javaArgs,
['--add-exports', 'java.base/sun.nio.ch=ALL-UNNAMED'],
'mac|linux applies on macOS'
);
assert.deepStrictEqual(
categorize(fns, orArg, 'linux').javaArgs,
['--add-exports', 'java.base/sun.nio.ch=ALL-UNNAMED'],
'mac|linux applies on Linux'
);
assert.deepStrictEqual(
categorize(fns, orArg, 'win32'),
{ javaArgs: [], programArgs: [] },
'mac|linux is dropped on Windows'
);

// --- Plain -D / -X pass through untouched; program args go after the jar ---
assert.deepStrictEqual(
categorize(fns, ['-Dfoo=bar', '-Xmx1g', 'myProgramArg'], 'linux'),
{ javaArgs: ['-Dfoo=bar', '-Xmx1g'], programArgs: ['myProgramArg'] },
'plain -D/-X are JVM args; non-dash tokens are program args'
);

// --- Value-less double-dash flags are kept as a single JVM token ---
assert.deepStrictEqual(
categorize(fns, ['--enable-preview'], 'linux'),
{ javaArgs: ['--enable-preview'], programArgs: [] },
'value-less --enable-preview stays a single token'
);

// --- Short module-path form -p is split into -p + value ---
assert.deepStrictEqual(
categorize(fns, ['-p /some/modules'], 'linux'),
{ javaArgs: ['-p', '/some/modules'], programArgs: [] },
'-p <path> is split into two JVM tokens'
);

// --- Non-array / empty inputs are tolerated ---
assert.deepStrictEqual(
categorize(fns, [], 'linux'),
{ javaArgs: [], programArgs: [] },
'empty args produce no output'
);
(function () {
const javaArgs = [];
const programArgs = [];
fns.appendPackageArgs(undefined, javaArgs, programArgs);
assert.deepStrictEqual({ javaArgs, programArgs }, { javaArgs: [], programArgs: [] },
'undefined args are tolerated');
})();

console.log('package-args test passed');
}

main();
Loading
Loading