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
Original file line number Diff line number Diff line change
Expand Up @@ -458,10 +458,17 @@ private String getGradleJavaHome() throws BuildException {
);
}
}
if (home == null && currentJvmIsJava17OrLater()) {
// The Maven plugin itself is already running on a JDK that Gradle 8 accepts
// (Java 17+). Use that JVM's home instead of forcing the user to duplicate
// the JDK location in JAVA17_HOME.
return System.getProperty("java.home");
}
if (home == null) {
throw new BuildException(
"When using gradle 8, " +
"you must set the JAVA17_HOME environment variable to the location of a Java 17 JDK"
"you must set the JAVA17_HOME environment variable to the location of a Java 17 JDK " +
"(or run Maven on Java 17+ and let the build reuse the current JVM)"
);
}

Expand All @@ -473,6 +480,19 @@ private String getGradleJavaHome() throws BuildException {
}
return System.getProperty("java.home");
}

private static boolean currentJvmIsJava17OrLater() {
String spec = System.getProperty("java.specification.version", "");
if (spec.startsWith("1.")) {
// 1.5 .. 1.8 era — definitely older than 17.
return false;
}
try {
return Integer.parseInt(spec) >= 17;
} catch (NumberFormatException ex) {
return false;
}
}
private int parseVersionStringAsInt(String versionString) {
if (versionString.indexOf(".") > 0) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,24 @@ public void generateCode(File destination, boolean overwrite) throws IOException
log.debug(javaseFile+" already exists. Skipping");
}

if(overwrite || !csFile.exists()) {
log.info("Writing "+csFile);
generateCSFile(csFile, "FrameworkElement");
// Only emit the UWP / Windows C# stub if the project actually has a win/
// module. Java 17 generated projects no longer ship that module (the
// Windows native port is legacy), so omitting the stub avoids leaving
// dangling .cs files outside any compiled source set.
File winModuleRoot = csFile.getParentFile();
while (winModuleRoot != null && !"win".equals(winModuleRoot.getName())) {
winModuleRoot = winModuleRoot.getParentFile();
}
boolean winModulePresent = winModuleRoot != null && winModuleRoot.isDirectory();
if (winModulePresent) {
if(overwrite || !csFile.exists()) {
log.info("Writing "+csFile);
generateCSFile(csFile, "FrameworkElement");
} else {
log.debug(csFile+" already exists. Skipping");
}
} else {
log.debug(csFile+" already exists. Skipping");
log.debug("No win/ module under destination — skipping C# stub for "+nativeInterface.getName());
}
if(overwrite || !(iosHFile.exists() || iosMFile.exists())) {
log.info("Writing "+iosHFile);
Expand Down
38 changes: 38 additions & 0 deletions scripts/initializr/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,22 @@
</profiles>

<build>
<resources>
<!--
Codename One's classloader rejects nested directories under
src/main/resources at runtime ("resources cannot be nested in
directories" - see CN1 ClassPathLoader). The on-disk skill/
folder is for editing/version control only; it is repackaged
into skill.zip by the antrun execution below before it reaches
the JAR.
-->
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>skill/**</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down Expand Up @@ -402,6 +418,28 @@
</target>
</configuration>
</execution>
<!--
Build skill.zip from src/main/resources/skill/** so the
Codename One authoring skill ships as a single flat
resource. CN1's classloader does not allow nested
directories under src/main/resources at runtime, so we
package the nested markdown into a zip at build time and
stream entries out in GeneratorModel.
-->
<execution>
<id>package-claude-skill</id>
<phase>process-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<zip destfile="${project.build.outputDirectory}/skill.zip">
<fileset dir="${project.basedir}/src/main/resources/skill"/>
</zip>
</target>
</configuration>
</execution>
</executions>
</plugin>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void runApp() {
final boolean[] roundedButtons = new boolean[]{true};
final boolean[] includeLocalizationBundles = new boolean[]{false};
final ProjectOptions.PreviewLanguage[] previewLanguage = new ProjectOptions.PreviewLanguage[]{ProjectOptions.PreviewLanguage.ENGLISH};
final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_8};
final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_17};
final String[] customThemeCss = new String[]{""};
final String[] lastValidCustomThemeCss = new String[]{""};
final TextArea[] customCssEditorRef = new TextArea[1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,53 @@ public class GeneratorModel {
".DS_Store\n" +
"Thumbs.db\n";

private static final String AGENT_SKILL_TARGET_PREFIX = ".agent-skills/codename-one/";
private static final String CLAUDE_SKILL_STUB_PATH = ".claude/skills/codename-one/SKILL.md";
private static final String CLAUDE_SKILL_STUB_BODY =
"---\n"
+ "name: codename-one\n"
+ "description: Build and modify Codename One cross-platform mobile apps (Java 17, Maven, ParparVM/Android/iOS/JavaScript). Use when the project contains a `common/codenameone_settings.properties`, depends on `com.codenameone:codenameone-core`, edits CSS files under `common/src/main/css/`, calls `cn1:run`, `cn1:test`, `cn1:build`, references `com.codename1.ui.*` / `com.codename1.testing.*`, or when the user asks to build a UI, write screen tests, generate screenshots, or compare to Swing/HTML/Android.\n"
+ "metadata:\n"
+ " type: skill\n"
+ "---\n"
+ "\n"
+ "# Codename One — App and UI Authoring Skill (Claude Code stub)\n"
+ "\n"
+ "This file exists so Claude Code can index the Codename One authoring skill.\n"
+ "The actual skill content is **vendor-neutral** and lives in this repository at:\n"
+ "\n"
+ "- `.agent-skills/codename-one/SKILL.md` — top-level cheat sheet\n"
+ "- `.agent-skills/codename-one/references/*.md` — deep-dive references\n"
+ "- `.agent-skills/codename-one/tools/` — runnable Java 17 utilities (`isApiSupported`, `isCssValid`, ...)\n"
+ "\n"
+ "**Read `.agent-skills/codename-one/SKILL.md` next.** All the guidance you need to\n"
+ "build, style, test, debug, and port to Codename One is in that directory.\n";
private static final String AGENTS_MD_BODY =
"# AGENTS.md\n"
+ "\n"
+ "This project is a Codename One cross-platform mobile app (Java 17 / Maven /\n"
+ "ParparVM-iOS / Android / JavaScript / desktop). A vendor-neutral authoring skill\n"
+ "is bundled in this repository for any AI agent:\n"
+ "\n"
+ "- **Start here:** `.agent-skills/codename-one/SKILL.md`\n"
+ "- **Topical references:** `.agent-skills/codename-one/references/`\n"
+ "- **Runnable utilities (Java 17 single-file source mode):** `.agent-skills/codename-one/tools/`\n"
+ "\n"
+ "Tool integrations (Claude Code, Cursor, etc.) may also pick this skill up via\n"
+ "their own conventions; the canonical source of truth is `.agent-skills/`.\n"
+ "\n"
+ "## Quick orientation for an agent\n"
+ "\n"
+ "- App source lives in `common/src/main/java/`.\n"
+ "- Theme/styling lives in `common/src/main/css/theme.css` (Codename One CSS — a\n"
+ " deliberate subset, see `.agent-skills/codename-one/references/css.md`).\n"
+ "- Run the simulator with `mvn -pl common cn1:run`.\n"
+ "- Run tests with `mvn -pl common cn1:test` (on Linux CI use `xvfb-run -a`).\n"
+ "- Native cloud builds use `mvn -pl <ios|android|javascript|javase> package -Dcodename1.platform=... -Dcodename1.buildTarget=...`.\n"
+ "\n"
+ "When in doubt, open `.agent-skills/codename-one/SKILL.md` and follow the\n"
+ "reference table at the bottom.\n";

private final IDE ide;
private final Template template;
private final String appName;
Expand Down Expand Up @@ -83,6 +130,9 @@ void writeProjectZip(OutputStream outputStream) throws IOException {
copyZipEntriesToMap("/common.zip", mergedEntries, ZipEntryType.COMMON);
copySingleTextEntryToMap(".gitignore", GENERATED_GITIGNORE, mergedEntries, ZipEntryType.COMMON);
copySingleTextEntryToMap("README.md", buildReadmeMarkdown(), mergedEntries, ZipEntryType.COMMON);
if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17) {
addAgentSkillEntries(mergedEntries);
}
copySingleTextEntryToMap("common/pom.xml", readResourceToString(template.POM_XML), mergedEntries, ZipEntryType.TEMPLATE_POM);
if (template.CN1LIB_ZIP != null) {
copyZipEntriesToMap(template.CN1LIB_ZIP, mergedEntries, ZipEntryType.TEMPLATE_CN1LIB);
Expand All @@ -102,6 +152,38 @@ void writeProjectZip(OutputStream outputStream) throws IOException {
}


private void addAgentSkillEntries(Map<String, byte[]> mergedEntries) throws IOException {
// Ship the Codename One authoring skill inside every generated project under a
// vendor-neutral path so any AI agent (Claude Code, Cursor, others) can pick it up.
// The skill markdown lives in source form under src/main/resources/skill/** and is
// repackaged into skill.zip at build time (CN1 classloader rejects nested
// directories under resources). ASCII-only Markdown so no encoding surprises end
// up in the project tree.
try (ZipInputStream zis = new ZipInputStream(getResourceAsStream("/skill.zip"))) {
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
if (!entry.isDirectory()) {
byte[] sourceData = readToBytesNoClose(zis);
String relative = entry.getName();
// Defensive normalization: ant may produce backslashes on Windows.
relative = StringUtil.replaceAll(relative, "\\", "/");
String targetPath = AGENT_SKILL_TARGET_PREFIX + relative;
byte[] targetData = applyDataReplacements(targetPath, sourceData);
mergedEntries.put(targetPath, targetData);
}
zis.closeEntry();
entry = zis.getNextEntry();
}
}
// Top-level AGENTS.md so agents that follow the (emerging) AGENTS.md convention
// discover the skill without having to know our directory layout.
copySingleTextEntryToMap("AGENTS.md", AGENTS_MD_BODY, mergedEntries, ZipEntryType.COMMON);
// Claude Code stub. Frontmatter so the skill shows up in /skills, body redirects
// to the canonical vendor-neutral content.
copySingleTextEntryToMap(CLAUDE_SKILL_STUB_PATH, CLAUDE_SKILL_STUB_BODY,
mergedEntries, ZipEntryType.COMMON);
}

private void addLocalizationEntries(Map<String, byte[]> mergedEntries) throws IOException {
if (!isBareTemplate() || !options.includeLocalizationBundles) {
return;
Expand Down Expand Up @@ -130,11 +212,21 @@ private void addLocalizationEntries(Map<String, byte[]> mergedEntries) throws IO
}

private void copyZipEntriesToMap(String zipResource, Map<String, byte[]> mergedEntries, ZipEntryType zipType) throws IOException {
boolean dropWindowsModule = options.javaVersion == ProjectOptions.JavaVersion.JAVA_17;
try(ZipInputStream zis = new ZipInputStream(getResourceAsStream(zipResource))) {
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
if (!entry.isDirectory()) {
copyEntryToMap(entry.getName(), readToBytesNoClose(zis), mergedEntries, zipType);
String name = entry.getName();
if (dropWindowsModule && (name.startsWith("win/") || name.startsWith("win\\"))) {
// Java 17 projects don't ship the UWP/Windows native module — strip
// its source tree from the extracted skeleton. The matching root-pom
// <profile id="win"> block is removed in applyDataReplacements below.
zis.closeEntry();
entry = zis.getNextEntry();
continue;
}
copyEntryToMap(name, readToBytesNoClose(zis), mergedEntries, zipType);
}
zis.closeEntry();
entry = zis.getNextEntry();
Expand Down Expand Up @@ -220,6 +312,9 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw
if ("pom.xml".equals(targetPath)) {
content = replaceTagValue(content, "cn1.plugin.version", CN1_PLUGIN_VERSION);
content = replaceTagValue(content, "cn1.version", CN1_PLUGIN_VERSION);
if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17) {
content = stripWindowsModuleProfile(content);
}
}
if ("android/pom.xml".equals(targetPath) || "ios/pom.xml".equals(targetPath) || "javascript/pom.xml".equals(targetPath)) {
content = hardenPlatformModulePomAgainstDoubleJarAttach(content);
Expand All @@ -233,14 +328,14 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw


private String applyJavaVersionSettings(String content) {
if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL) {
if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17) {
content = replaceProperty(content, "codename1.arg.java.version", "17");
}
return content;
}

private String applyJavaVersionToPom(String content) {
if (options.javaVersion != ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL) {
if (options.javaVersion != ProjectOptions.JavaVersion.JAVA_17) {
return content;
}
content = StringUtil.replaceAll(content, "<source>1.8</source>", "<source>17</source>");
Expand All @@ -249,7 +344,7 @@ private String applyJavaVersionToPom(String content) {
}

private String normalizeIntellijMiscXml(String content) {
String languageLevel = options.javaVersion == ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL ? "JDK_17" : "JDK_1_8";
String languageLevel = options.javaVersion == ProjectOptions.JavaVersion.JAVA_17 ? "JDK_17" : "JDK_1_8";
content = removeXmlAttribute(content, "project-jdk-name");
content = removeXmlAttribute(content, "project-jdk-type");
content = setXmlAttribute(content, "languageLevel", languageLevel);
Expand Down Expand Up @@ -415,6 +510,34 @@ private static String replaceTagValue(String xml, String tagName, String value)
return xml.substring(0, valueStart) + value + xml.substring(end);
}

private static String stripWindowsModuleProfile(String pom) {
// Remove the <profile><id>win</id>... </profile> block from the root pom.
// Tolerant of leading whitespace and any inner content. The win/ source
// tree is also dropped from the zip extraction (see copyZipEntriesToMap).
int idIdx = pom.indexOf("<id>win</id>");
if (idIdx < 0) {
return pom;
}
int profileStart = pom.lastIndexOf("<profile>", idIdx);
if (profileStart < 0) {
return pom;
}
int profileEnd = pom.indexOf("</profile>", idIdx);
if (profileEnd < 0) {
return pom;
}
profileEnd += "</profile>".length();
// Swallow trailing newline if present so we don't leave an empty line.
while (profileEnd < pom.length() && (pom.charAt(profileEnd) == '\n' || pom.charAt(profileEnd) == '\r')) {
profileEnd++;
}
// Also swallow leading whitespace on the line containing <profile>.
while (profileStart > 0 && (pom.charAt(profileStart - 1) == ' ' || pom.charAt(profileStart - 1) == '\t')) {
profileStart--;
}
return pom.substring(0, profileStart) + pom.substring(profileEnd);
}

private static String normalizeJavasePom(String pom) {
pom = removeDependencyBlock(pom, "com.codenameone", "codenameone-core", "provided");
pom = removeDependencyBlock(pom, "com.codenameone", "codenameone-javase", "provided");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public enum Accent {
}

public enum JavaVersion {
JAVA_8("Java 8"),
JAVA_17_EXPERIMENTAL("Java 17 (Experimental)");
JAVA_17("Java 17"),
JAVA_8("Java 8");

public final String label;

Expand Down Expand Up @@ -81,11 +81,11 @@ public ProjectOptions(ThemeMode themeMode, Accent accent, boolean roundedButtons
this.roundedButtons = roundedButtons;
this.includeLocalizationBundles = includeLocalizationBundles;
this.previewLanguage = previewLanguage == null ? PreviewLanguage.ENGLISH : previewLanguage;
this.javaVersion = javaVersion == null ? JavaVersion.JAVA_8 : javaVersion;
this.javaVersion = javaVersion == null ? JavaVersion.JAVA_17 : javaVersion;
this.customThemeCss = customThemeCss;
}

public static ProjectOptions defaults() {
return new ProjectOptions(ThemeMode.LIGHT, Accent.DEFAULT, true, false, PreviewLanguage.ENGLISH, JavaVersion.JAVA_8);
return new ProjectOptions(ThemeMode.LIGHT, Accent.DEFAULT, true, false, PreviewLanguage.ENGLISH, JavaVersion.JAVA_17);
}
}
Loading
Loading