From 39acb23bb9405387f04499b06f270077759cb425 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:59:12 +0200
Subject: [PATCH 1/3] iOS preflight: refuse CN1Widgets with no provisioning
profile under manual signing (#6008)
An app that names com.codename1.surfaces gets a generated CN1Widgets
target that needs its own App ID (with App Groups) and profile. With only
the app's profile the cloud build ran until xcodebuild failed on the
bundle-ID mismatch. The preflight now scans the staged jar for the same
signal IPhoneBuilder uses and reports the App ID, the setting/hint that
supply the profile and the Certificate Wizard before a build is spent.
Co-Authored-By: Claude Opus 5.5
---
.../java/com/codename1/maven/AppBuilder.java | 10 +-
.../maven/IOSProvisioningPreflight.java | 125 ++++++++++++++++--
.../IOSAppExtensionSigningPreflightTest.java | 79 +++++++++++
3 files changed, 197 insertions(+), 17 deletions(-)
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java
index 6dffd18a586..ff06e6c3ad0 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java
@@ -634,7 +634,8 @@ private boolean isIOSDeviceBuild() {
* property. This is where a profile-kind/distribution-method mismatch is decided, since
* a library can still change the method after the early pass has run.
*/
- private void applyIOSProvisioningPreflight(Properties mergedSettings) throws BuildFailureException {
+ private void applyIOSProvisioningPreflight(Properties mergedSettings, File stagedJar)
+ throws BuildFailureException {
if (!isIOSDeviceBuild()) {
return;
}
@@ -647,7 +648,10 @@ private void applyIOSProvisioningPreflight(Properties mergedSettings) throws Bui
// the ios module under Maven, src/ios under Gradle, native/ios under Ant.
report(IOSProvisioningPreflight.checkAppExtensions(mergedSettings, release,
host.layout().iosAppExtensionsDir().getParentFile()));
- report(IOSProvisioningPreflight.checkGeneratedExtensions(mergedSettings, release));
+ // CN1Widgets is generated whenever a class names com.codename1.surfaces, so the staged
+ // classes decide it, the same scan the builder makes.
+ report(IOSProvisioningPreflight.checkGeneratedExtensions(mergedSettings, release,
+ IOSProvisioningPreflight.referencesSurfaces(stagedJar)));
report(IOSProvisioningPreflight.checkContinuitySync(mergedSettings, release));
}
@@ -2016,7 +2020,7 @@ private void createAntProject() throws IOException, LibraryPropertiesException,
// Same reason, for the same reason: a CN1Lib can supply ios.*.distributionMethod, so the
// profile's kind is compared against the export method only now that those properties
// have been merged -- an early refusal would reject a build the merge was about to fix.
- applyIOSProvisioningPreflight(cn1SettingsProps);
+ applyIOSProvisioningPreflight(cn1SettingsProps, jarWithDependencies);
// Validate the exact settings and classes sent to every cloud/local target before
// invoking the build client. A renamed entry point must not cost a cloud build.
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java b/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
index 199e459fb83..14a11d2166d 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
@@ -434,8 +434,9 @@ static List checkAppExtensions(Properties settings, boolean release, Fi
* build and an Xcode failure minutes into a cloud one, naming a bundle id the developer never
* typed.
*
- * Only the document provider is checked here. The other generated extensions (the Wallet
- * pair, CN1Widgets, CN1MatterSetup) have exactly this shape and could join the table below,
+ *
The document provider is checked here, and CN1Widgets by
+ * {@link #checkGeneratedExtensions(Properties, boolean, boolean)}. The other generated
+ * extensions (the Wallet pair, CN1MatterSetup) have exactly this shape and could join them,
* but each one turns a late cloud failure into an early local one for projects that build
* today, so they are left for a change that can be verified against real projects rather than
* folded in as a side effect.
@@ -463,6 +464,97 @@ static List checkAppExtensions(Properties settings, boolean release, Fi
/// feature behind a hint the developer set deliberately, having first
/// obtained a capability Apple grants by hand.
static List checkGeneratedExtensions(Properties settings, boolean release) {
+ return checkGeneratedExtensions(settings, release, false);
+ }
+
+ /// The same, plus the CN1Widgets WidgetKit target when `usesSurfaces` says the app references
+ /// `com.codename1.surfaces` (see [#referencesSurfaces]).
+ ///
+ /// Unlike the document provider, nothing in the settings enables CN1Widgets: the builder
+ /// generates it whenever a class names the surfaces package, so the caller has to scan the
+ /// staged classes for the same signal. Issue #6008 is the real-project evidence the note above
+ /// asked for -- an app supplying only its own profile ran a whole cloud build to have Xcode
+ /// report that profile's App ID "does not match the bundle ID .CN1Widgets", a
+ /// message that names neither the extension's hint nor the Certificate Wizard.
+ ///
+ /// No wildcard exemption, for the reason the document provider gives: the widget target
+ /// always declares the App Group its timeline travels through, and a wildcard App ID cannot
+ /// carry App Groups.
+ static List checkGeneratedExtensions(Properties settings, boolean release,
+ boolean usesSurfaces) {
+ List problems = checkDocumentProvider(settings, release);
+ if (settings == null || !usesSurfaces
+ || "false".equals(trimmed(settings.getProperty("codename1.arg.ios.surfaces.extension")))) {
+ // ios.surfaces.extension=false skips the whole iOS lowering: no target, nothing to sign.
+ return problems;
+ }
+ String packageName = trimmed(settings.getProperty("codename1.packageName"));
+ if (packageName == null || packageName.isEmpty()) {
+ return problems;
+ }
+ String name = "CN1Widgets";
+ if (hasOwnProfileSetting(settings, name, release)) {
+ return problems;
+ }
+ Profile appProfile = appProfile(settings, release);
+ if (appProfile == null || appProfile.applicationIdentifier == null) {
+ // check() reports a missing or unreadable app profile as itself.
+ return problems;
+ }
+ String bundleId = generatedExtensionBundleId(settings, "ios.surfaces", packageName, name);
+ String buildType = release ? "release" : "debug";
+ problems.add(new Problem("This app uses com.codename1.surfaces (home-screen widgets or "
+ + "live activities), so the iOS build adds the " + name + " WidgetKit extension, "
+ + "and no provisioning profile is configured for it. The profile this build signs "
+ + "with (\"" + appProfile.name + "\", App ID "
+ + appIdPattern(appProfile.applicationIdentifier) + ") cannot sign it: Xcode would "
+ + "fail with: Provisioning profile \"" + appProfile.name + "\" has app ID \""
+ + appIdPattern(appProfile.applicationIdentifier) + "\", which does not match the "
+ + "bundle ID \"" + bundleId + "\".\n"
+ + "Create an App ID for " + bundleId + " with the App Groups capability in the "
+ + "Apple Developer portal, generate a provisioning profile for it against the same "
+ + "certificate as the app, and supply it in one of these ways:\n"
+ + " 1. Set codename1.ios." + buildType + ".appext." + name + ".provision="
+ + "{path to the .mobileprovision} in codenameone_settings.properties.\n"
+ + " 2. Host the profile and name its URL in codename1.arg.ios.appext." + name
+ + ".provisioningURL.\n"
+ + "The Certificate Wizard does all of this for you. To build without widgets, set "
+ + "codename1.arg.ios.surfaces.extension=false.", true));
+ return problems;
+ }
+
+ /// Whether any class in `jar` outside the surfaces package itself names
+ /// `com/codename1/surfaces/` -- the signal IPhoneBuilder sets `usesSurfaces` from. The staged
+ /// jar carries the app and its cn1libs but not the core, so the package's own classes are only
+ /// there when a library bundles them, and those are skipped. A jar that cannot be read answers
+ /// false: this is a preflight, and an IO error is not a signing problem.
+ static boolean referencesSurfaces(File jar) {
+ if (jar == null || !jar.isFile()) {
+ return false;
+ }
+ byte[] needle = "com/codename1/surfaces/".getBytes(StandardCharsets.US_ASCII);
+ try (ZipFile zip = new ZipFile(jar)) {
+ Enumeration extends ZipEntry> entries = zip.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ String entryName = entry.getName();
+ if (entry.isDirectory() || !entryName.endsWith(".class")
+ || entryName.startsWith("com/codename1/surfaces/")) {
+ continue;
+ }
+ try (InputStream in = zip.getInputStream(entry)) {
+ if (indexOf(readAll(in), needle, 0) >= 0) {
+ return true;
+ }
+ }
+ }
+ } catch (IOException ex) {
+ return false;
+ }
+ return false;
+ }
+
+ private static List checkDocumentProvider(Properties settings, boolean release) {
List problems = new ArrayList();
if (settings == null) {
return problems;
@@ -507,7 +599,8 @@ static List checkGeneratedExtensions(Properties settings, boolean relea
// neither is something to refuse a build over here.
return problems;
}
- String bundleId = generatedExtensionBundleId(settings, packageName, name);
+ String bundleId = generatedExtensionBundleId(settings, "ios.documentProvider", packageName,
+ name);
if (profileCoversBundleId(appProfile.applicationIdentifier, bundleId)) {
// No wildcard exemption here, unlike the folder-driven check above. A wildcard App ID
// does cover the bundle id, but this extension always declares
@@ -588,7 +681,7 @@ private static Problem appGroupProblem(Properties settings, String name, boolean
// report, not this one's.
return null;
}
- String bundleId = generatedExtensionBundleId(settings,
+ String bundleId = generatedExtensionBundleId(settings, "ios.documentProvider",
trimmed(settings.getProperty("codename1.packageName")), name);
if (!profileCoversBundleId(profile.applicationIdentifier, bundleId)) {
// A profile for a DIFFERENT App ID that happens to grant the same group. It is
@@ -624,9 +717,9 @@ private static Problem appGroupProblem(Properties settings, String name, boolean
/// the profile against the default would reject the profile issued for the real identifier
/// and accept one issued for a target that is not being built -- the failure moving to Xcode,
/// after the archive, which is what this whole class exists to prevent.
- private static String generatedExtensionBundleId(Properties settings, String packageName,
- String name) {
- String override = settings.getProperty("codename1.arg.ios.documentProvider.buildSettings."
+ private static String generatedExtensionBundleId(Properties settings, String hintPrefix,
+ String packageName, String name) {
+ String override = settings.getProperty("codename1.arg." + hintPrefix + ".buildSettings."
+ "PRODUCT_BUNDLE_IDENTIFIER");
if (override != null) {
String resolved = resolvePlaceholders(override.trim(), settings);
@@ -1492,13 +1585,17 @@ private static int indexOf(byte[] haystack, byte[] needle, int from) {
private static byte[] readFile(File f) throws IOException {
try (InputStream is = new FileInputStream(f)) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- byte[] buffer = new byte[8192];
- int amount;
- while ((amount = is.read(buffer)) > -1) {
- baos.write(buffer, 0, amount);
- }
- return baos.toByteArray();
+ return readAll(is);
+ }
+ }
+
+ private static byte[] readAll(InputStream is) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buffer = new byte[8192];
+ int amount;
+ while ((amount = is.read(buffer)) > -1) {
+ baos.write(buffer, 0, amount);
}
+ return baos.toByteArray();
}
}
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java
index 5099346646c..3548b28bb0d 100644
--- a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java
+++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java
@@ -567,4 +567,83 @@ public void appIdMatchingFollowsApplesRule() {
assertTrue(IOSProvisioningPreflight.profileCoversBundleId(null, "com.example.app"));
assertTrue(IOSProvisioningPreflight.profileCoversBundleId("", "com.example.app"));
}
+
+ // ---- CN1Widgets, generated whenever a class names com.codename1.surfaces (#6008) ----
+
+ private static List checkWidgets(Properties p,
+ boolean usesSurfaces) {
+ return IOSProvisioningPreflight.checkGeneratedExtensions(p, true, usesSurfaces);
+ }
+
+ @Test
+ public void surfacesWithOnlyTheAppProfileAreRefused() throws Exception {
+ Properties p = settings(profile("PROD", "ABCD1234.com.example.app", GROUP));
+ List problems = checkWidgets(p, true);
+ assertEquals(1, problems.size());
+ assertTrue(problems.get(0).fatal);
+ String message = problems.get(0).message;
+ assertTrue(message, message.contains("com.example.app.CN1Widgets"));
+ assertTrue(message, message.contains("App Groups"));
+ assertTrue(message, message.contains("codename1.ios.release.appext.CN1Widgets.provision"));
+ assertTrue(message, message.contains("ios.appext.CN1Widgets.provisioningURL"));
+ assertTrue(message, message.contains("Certificate Wizard"));
+ }
+
+ @Test
+ public void surfacesWithAWidgetProfileAreAccepted() throws Exception {
+ Properties p = settings(profile("PROD", "ABCD1234.com.example.app", GROUP));
+ p.setProperty("codename1.ios.appext.CN1Widgets.provision",
+ profile("Widgets", "ABCD1234.com.example.app.CN1Widgets", GROUP).getAbsolutePath());
+ assertTrue(checkWidgets(p, true).isEmpty());
+
+ Properties hinted = settings(profile("PROD2", "ABCD1234.com.example.app", GROUP));
+ hinted.setProperty("codename1.arg.ios.appext.CN1Widgets.provisioningURL",
+ "https://example.com/widgets.mobileprovision");
+ assertTrue(checkWidgets(hinted, true).isEmpty());
+ }
+
+ @Test
+ public void anAppThatNeverNamesSurfacesIsNotAskedForAWidgetProfile() throws Exception {
+ Properties p = settings(profile("PROD", "ABCD1234.com.example.app", GROUP));
+ assertTrue(checkWidgets(p, false).isEmpty());
+ // ios.surfaces.extension=false skips the target, so there is nothing to sign either.
+ p.setProperty("codename1.arg.ios.surfaces.extension", "false");
+ assertTrue(checkWidgets(p, true).isEmpty());
+ }
+
+ @Test
+ public void automaticSigningTargetsAreNotChecked() {
+ // ios-source is an Xcode project the developer signs (automatically or otherwise), and the
+ // simulator signs nothing: the preflight never runs for either.
+ assertFalse(IOSProvisioningPreflight.appliesTo("ios", "ios-source"));
+ assertFalse(IOSProvisioningPreflight.appliesTo("ios", "ios-simulator"));
+ assertTrue(IOSProvisioningPreflight.appliesTo("ios", "ios-device"));
+ }
+
+ @Test
+ public void theSurfacesScanReadsTheStagedClasses() throws Exception {
+ File uses = tmp.newFile("uses.jar");
+ writeJar(uses, "com/example/app/Main.class",
+ "....Lcom/codename1/surfaces/ExternalSurfaces;....");
+ assertTrue(IOSProvisioningPreflight.referencesSurfaces(uses));
+
+ // A bundled copy of the package itself is not a use of it.
+ File bundled = tmp.newFile("bundled.jar");
+ writeJar(bundled, "com/codename1/surfaces/ExternalSurfaces.class",
+ "com/codename1/surfaces/SurfaceKind");
+ assertFalse(IOSProvisioningPreflight.referencesSurfaces(bundled));
+
+ assertFalse(IOSProvisioningPreflight.referencesSurfaces(new File(tmp.getRoot(), "none.jar")));
+ }
+
+ private static void writeJar(File jar, String entry, String contents) throws Exception {
+ ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(jar));
+ try {
+ zip.putNextEntry(new ZipEntry(entry));
+ zip.write(contents.getBytes("UTF-8"));
+ zip.closeEntry();
+ } finally {
+ zip.close();
+ }
+ }
}
From b7de2fb0c0f8aa35a2a2b8bb698429c8d44d39c9 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Mon, 26 Oct 2026 18:14:57 +0200
Subject: [PATCH 2/3] Preflight: count only class references to surfaces, judge
a path-valued widget profile's App ID
Co-Authored-By: Claude Opus 5.5
---
.../maven/IOSProvisioningPreflight.java | 141 ++++++++++++++++--
.../IOSAppExtensionSigningPreflightTest.java | 96 ++++++++++--
2 files changed, 218 insertions(+), 19 deletions(-)
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java b/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
index 14a11d2166d..4173969c635 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
@@ -494,6 +494,21 @@ static List checkGeneratedExtensions(Properties settings, boolean relea
}
String name = "CN1Widgets";
if (hasOwnProfileSetting(settings, name, release)) {
+ // Only a profile given as a local PATH can be read; a hosted URL or base64 blob is
+ // judged by the server. The App Group is not checked here: it comes from
+ // surfaces.json, which is a resource of the app rather than a setting.
+ Profile ext = extensionProfileFromPath(settings, name, release);
+ String bundleId = generatedExtensionBundleId(settings, "ios.surfaces", packageName,
+ name);
+ if (ext != null && ext.applicationIdentifier != null
+ && !profileCoversBundleId(ext.applicationIdentifier, bundleId)) {
+ problems.add(new Problem("The provisioning profile configured for the " + name
+ + " extension (\"" + ext.name + "\", App ID "
+ + appIdPattern(ext.applicationIdentifier) + ") cannot sign its bundle ID \""
+ + bundleId + "\". Generate a profile for the App ID " + bundleId
+ + " (with App Groups) and configure that one instead; the Certificate "
+ + "Wizard does this for you.", true));
+ }
return problems;
}
Profile appProfile = appProfile(settings, release);
@@ -523,27 +538,58 @@ static List checkGeneratedExtensions(Properties settings, boolean relea
return problems;
}
- /// Whether any class in `jar` outside the surfaces package itself names
- /// `com/codename1/surfaces/` -- the signal IPhoneBuilder sets `usesSurfaces` from. The staged
- /// jar carries the app and its cn1libs but not the core, so the package's own classes are only
- /// there when a library bundles them, and those are skipped. A jar that cannot be read answers
- /// false: this is a preflight, and an IO error is not a signing problem.
+ /// The extension profile configured for this build type as a local path, or null when there
+ /// is none or it cannot be read.
+ private static Profile extensionProfileFromPath(Properties settings, String name,
+ boolean release) {
+ String qualifier = release ? "release" : "debug";
+ String[] keys = {
+ "codename1.ios." + qualifier + ".appext." + name + ".provision",
+ "codename1.ios.appext." + name + ".provision"
+ };
+ for (String key : keys) {
+ String value = trimmed(settings.getProperty(key));
+ if (value == null || value.isEmpty()) {
+ continue;
+ }
+ String resolved = resolvePlaceholders(value, settings);
+ File file = new File(resolved);
+ if (resolved.indexOf("${") >= 0 || !file.isFile()) {
+ return null;
+ }
+ try {
+ return parse(readFile(file));
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /// Whether any class in `jar` REFERENCES a type in `com.codename1.surfaces` -- the signal
+ /// IPhoneBuilder sets `usesSurfaces` from, which is its class-reference callback. So this reads
+ /// the constant pool and counts only CONSTANT_Class names and descriptors (including those of
+ /// the class's own fields and methods), never a string literal that merely spells the package: a false match here is
+ /// a fatal refusal. The staged jar carries the app and its cn1libs but not the core.
+ ///
+ /// A missing surfaces.json is deliberately not reported here: the builder refuses that with
+ /// its own message, and this check cannot read the app's resources.
+ ///
+ /// A jar or class that cannot be read answers false: this is a preflight, and the server
+ /// refuses the same build before Xcode runs, so a miss costs a late failure, never a wrong one.
static boolean referencesSurfaces(File jar) {
if (jar == null || !jar.isFile()) {
return false;
}
- byte[] needle = "com/codename1/surfaces/".getBytes(StandardCharsets.US_ASCII);
try (ZipFile zip = new ZipFile(jar)) {
Enumeration extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
- String entryName = entry.getName();
- if (entry.isDirectory() || !entryName.endsWith(".class")
- || entryName.startsWith("com/codename1/surfaces/")) {
+ if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
continue;
}
try (InputStream in = zip.getInputStream(entry)) {
- if (indexOf(readAll(in), needle, 0) >= 0) {
+ if (classReferencesSurfaces(readAll(in))) {
return true;
}
}
@@ -554,6 +600,81 @@ static boolean referencesSurfaces(File jar) {
return false;
}
+ private static final String SURFACES_PACKAGE = "com/codename1/surfaces/";
+
+ /// Walks one class file's constant pool. Malformed input answers false.
+ static boolean classReferencesSurfaces(byte[] b) {
+ try {
+ if (b.length < 10 || (b[0] & 0xff) != 0xCA || (b[1] & 0xff) != 0xFE) {
+ return false;
+ }
+ int count = u2(b, 8);
+ String[] utf8 = new String[count];
+ int[] classIdx = new int[count];
+ boolean[] literal = new boolean[count];
+ int nClass = 0;
+ int p = 10;
+ for (int i = 1; i < count; i++) {
+ int tag = b[p] & 0xff;
+ switch (tag) {
+ case 1: {
+ int len = u2(b, p + 1);
+ utf8[i] = new String(b, p + 3, len, StandardCharsets.UTF_8);
+ p += 3 + len;
+ break;
+ }
+ case 7:
+ classIdx[nClass++] = u2(b, p + 1);
+ p += 3;
+ break;
+ case 8:
+ literal[u2(b, p + 1)] = true;
+ p += 3;
+ break;
+ case 16: case 19: case 20:
+ p += 3;
+ break;
+ case 15:
+ p += 4;
+ break;
+ case 3: case 4: case 9: case 10: case 11: case 12: case 17: case 18:
+ p += 5;
+ break;
+ case 5: case 6:
+ p += 9;
+ i++;
+ break;
+ default:
+ return false;
+ }
+ }
+ for (int i = 0; i < nClass; i++) {
+ String name = utf8[classIdx[i]];
+ // An array class is spelled as a descriptor, "[Lcom/...;".
+ if (name != null && (name.startsWith(SURFACES_PACKAGE)
+ || name.indexOf("L" + SURFACES_PACKAGE) >= 0)) {
+ return true;
+ }
+ }
+ // Every descriptor -- of a referenced member, and of this class's own fields and
+ // methods, whose descriptors sit in the pool but are named from field_info and
+ // method_info rather than from a pool entry -- is a UTF-8 entry no string literal
+ // points at. A literal that happens to be shaped like a descriptor is skipped too.
+ for (int i = 1; i < count; i++) {
+ if (utf8[i] != null && !literal[i] && utf8[i].indexOf("L" + SURFACES_PACKAGE) >= 0) {
+ return true;
+ }
+ }
+ return false;
+ } catch (RuntimeException ex) {
+ return false;
+ }
+ }
+
+ private static int u2(byte[] b, int p) {
+ return ((b[p] & 0xff) << 8) | (b[p + 1] & 0xff);
+ }
+
private static List checkDocumentProvider(Properties settings, boolean release) {
List problems = new ArrayList();
if (settings == null) {
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java
index 3548b28bb0d..995e767e452 100644
--- a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java
+++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/IOSAppExtensionSigningPreflightTest.java
@@ -621,26 +621,104 @@ public void automaticSigningTargetsAreNotChecked() {
}
@Test
- public void theSurfacesScanReadsTheStagedClasses() throws Exception {
+ public void theSurfacesScanCountsClassReferencesOnly() throws Exception {
+ // A CONSTANT_Class naming the package: the app uses it.
+ assertTrue(IOSProvisioningPreflight.classReferencesSurfaces(
+ classFile(7, "com/codename1/surfaces/ExternalSurfaces")));
+ // A field or method descriptor naming it (NameAndType's descriptor).
+ assertTrue(IOSProvisioningPreflight.classReferencesSurfaces(
+ classFile(12, "(Lcom/codename1/surfaces/SurfaceKind;)V")));
+ // A descriptor of the class's OWN method, named from method_info rather than from the
+ // pool: implementing a getter that returns a surfaces type is a use.
+ assertTrue(IOSProvisioningPreflight.classReferencesSurfaces(
+ classFile(1, "()Lcom/codename1/surfaces/spi/SurfaceBridge;")));
+ // An array class.
+ assertTrue(IOSProvisioningPreflight.classReferencesSurfaces(
+ classFile(7, "[Lcom/codename1/surfaces/SurfaceKind;")));
+ // A string literal that only spells the package is not a use, and a false match here
+ // would refuse a build that does not generate the extension at all.
+ assertFalse(IOSProvisioningPreflight.classReferencesSurfaces(
+ classFile(8, "com/codename1/surfaces/ExternalSurfaces")));
+ assertFalse(IOSProvisioningPreflight.classReferencesSurfaces(
+ classFile(7, "com/example/app/Main")));
+ // Malformed input answers false rather than throwing.
+ assertFalse(IOSProvisioningPreflight.classReferencesSurfaces(new byte[] {1, 2, 3}));
+ byte[] truncated = classFile(7, "com/codename1/surfaces/ExternalSurfaces");
+ byte[] cut = new byte[truncated.length - 20];
+ System.arraycopy(truncated, 0, cut, 0, cut.length);
+ assertFalse(IOSProvisioningPreflight.classReferencesSurfaces(cut));
+ }
+
+ @Test
+ public void theSurfacesScanReadsTheStagedJar() throws Exception {
File uses = tmp.newFile("uses.jar");
writeJar(uses, "com/example/app/Main.class",
- "....Lcom/codename1/surfaces/ExternalSurfaces;....");
+ classFile(7, "com/codename1/surfaces/ExternalSurfaces"));
assertTrue(IOSProvisioningPreflight.referencesSurfaces(uses));
- // A bundled copy of the package itself is not a use of it.
- File bundled = tmp.newFile("bundled.jar");
- writeJar(bundled, "com/codename1/surfaces/ExternalSurfaces.class",
- "com/codename1/surfaces/SurfaceKind");
- assertFalse(IOSProvisioningPreflight.referencesSurfaces(bundled));
+ File not = tmp.newFile("not.jar");
+ writeJar(not, "com/example/app/Main.class",
+ classFile(8, "com/codename1/surfaces/ExternalSurfaces"));
+ assertFalse(IOSProvisioningPreflight.referencesSurfaces(not));
+ File corrupt = tmp.newFile("corrupt.jar");
+ write(corrupt, "not a zip");
+ assertFalse(IOSProvisioningPreflight.referencesSurfaces(corrupt));
assertFalse(IOSProvisioningPreflight.referencesSurfaces(new File(tmp.getRoot(), "none.jar")));
}
- private static void writeJar(File jar, String entry, String contents) throws Exception {
+ @Test
+ public void aWidgetProfileForAnotherAppIdIsRefused() throws Exception {
+ // Most plausibly the app's own profile configured for the extension by mistake.
+ File app = profile("PROD", "ABCD1234.com.example.app", GROUP);
+ Properties p = settings(app);
+ p.setProperty("codename1.ios.appext.CN1Widgets.provision", app.getAbsolutePath());
+ List problems = checkWidgets(p, true);
+ assertEquals(1, problems.size());
+ assertTrue(problems.get(0).fatal);
+ assertTrue(problems.get(0).message,
+ problems.get(0).message.contains("com.example.app.CN1Widgets"));
+ }
+
+ /// A minimal class file whose constant pool holds one UTF-8 entry and one entry of `tag`
+ /// pointing at it (1: none): 7 CONSTANT_Class, 8 CONSTANT_String, 12 NameAndType (as its descriptor).
+ private static byte[] classFile(int tag, String utf8) throws Exception {
+ java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream();
+ java.io.DataOutputStream out = new java.io.DataOutputStream(bytes);
+ out.writeInt(0xCAFEBABE);
+ out.writeShort(0);
+ out.writeShort(52);
+ out.writeShort(tag == 12 ? 4 : tag == 1 ? 2 : 3);
+ out.writeByte(1);
+ out.writeUTF(utf8);
+ if (tag == 1) {
+ // Only the UTF-8 entry, as a member descriptor sits in the pool.
+ } else if (tag == 12) {
+ out.writeByte(1);
+ out.writeUTF("run");
+ out.writeByte(12);
+ out.writeShort(2);
+ out.writeShort(1);
+ } else {
+ out.writeByte(tag);
+ out.writeShort(1);
+ }
+ out.writeShort(0x21);
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(0);
+ out.flush();
+ return bytes.toByteArray();
+ }
+
+ private static void writeJar(File jar, String entry, byte[] contents) throws Exception {
ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(jar));
try {
zip.putNextEntry(new ZipEntry(entry));
- zip.write(contents.getBytes("UTF-8"));
+ zip.write(contents);
zip.closeEntry();
} finally {
zip.close();
From ba2551727f4544c2a96d42e8163a87953b9e0e10 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Mon, 26 Oct 2026 18:16:16 +0200
Subject: [PATCH 3/3] Preflight: fall back to the unqualified widget profile
key when the qualified one is not a file
Co-Authored-By: Claude Opus 5.5
---
.../java/com/codename1/maven/IOSProvisioningPreflight.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java b/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
index 4173969c635..447d43460a8 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/IOSProvisioningPreflight.java
@@ -555,7 +555,7 @@ private static Profile extensionProfileFromPath(Properties settings, String name
String resolved = resolvePlaceholders(value, settings);
File file = new File(resolved);
if (resolved.indexOf("${") >= 0 || !file.isFile()) {
- return null;
+ continue;
}
try {
return parse(readFile(file));
@@ -660,6 +660,9 @@ static boolean classReferencesSurfaces(byte[] b) {
// methods, whose descriptors sit in the pool but are named from field_info and
// method_info rather than from a pool entry -- is a UTF-8 entry no string literal
// points at. A literal that happens to be shaped like a descriptor is skipped too.
+ // That also counts a generic Signature naming the package (a List of a surfaces type);
+ // such an app is using surfaces in every sense that matters, and the server-side
+ // refusal is the same either way.
for (int i = 1; i < count; i++) {
if (utf8[i] != null && !literal[i] && utf8[i].indexOf("L" + SURFACES_PACKAGE) >= 0) {
return true;