Skip to content

Give an app extension its build settings, so UIKit links - #5578

Open
shai-almog wants to merge 23 commits into
masterfrom
appext-framework-autolink
Open

Give an app extension its build settings, so UIKit links#5578
shai-almog wants to merge 23 commits into
masterfrom
appext-framework-autolink

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Mirrors codenameone/BuildDaemon#191 in the open-source builder, which is what a
local iOS build uses.

An iOS archive with a .ios.appext app extension fails at link time:

Undefined symbols for architecture arm64:
  "_OBJC_CLASS_$_UIView", referenced from:
       in 0.arm64.thinlto.o
  "_OBJC_CLASS_$_LAContext", referenced from:
       in 4.arm64.thinlto.o
ld: symbol(s) not found for architecture arm64

The extension's Ld line carries -framework Foundation and nothing else, while
the main app's compile line has -fmodules and the extension's does not. No
modules means no clang autolinking, so #import <UIKit/UIKit.h> never emits
-framework UIKit, and the generated target's frameworks phase has only what the
builder attaches by hand.

The settings that should have turned modules on are parsed out of a
pbxproj-shaped block, and the parser sliced the last character of the value
INSTEAD of dropping it:

if (val.endsWith(";")) { val = val.substring(val.length() - 1); }   // -> ";"

So every KEY = YES; line produced ";", which Xcode reads as off.
CLANG_ENABLE_OBJC_ARC went the same way, so the extension has been building as
MRC and leaking.

Dropping the semicolon exposes the second half, which never ran before. A quoted
value like "gnu++14" is re-emitted inside a Ruby string literal in the project
fixup script, where the kept quotes are a syntax error. So unwrap those too.

Note this flips ARC on for .ios.appext targets that have silently been MRC.
An extension written with explicit retain/release will now fail to compile and
needs CLANG_ENABLE_OBJC_ARC = NO in its buildSettings.properties (that file is
read with Properties.load and was never affected by the bug). Swift extensions
are unaffected.

mvn -pl codenameone-maven-plugin compile on JDK 8 is green.

🤖 Generated with Claude Code

Mirrors the cloud builder fix. The extension target's settings block is
parsed out of a pbxproj-shaped string, and the parser sliced the last
character of the value INSTEAD of dropping it, so every "KEY = YES;"
line produced ";". Xcode reads that as off: CLANG_ENABLE_MODULES never
took, clang compiled the extension without -fmodules, and without
modules there is no autolinking. The target's frameworks phase carries
Foundation alone, so an extension importing UIKit reached ld with
nothing to resolve _OBJC_CLASS_$_UIView against. CLANG_ENABLE_OBJC_ARC
was off for the same reason, so the extension built as MRC and leaked.

Dropping the semicolon exposes the second half, which never ran before:
a quoted value like "gnu++14" is re-emitted inside a Ruby string literal
in the project fixup script, where the kept quotes are a syntax error.
So unwrap those too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.01% (8889/98635 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.82% (45982/521538), branch 3.45% (1702/49369), complexity 3.43% (1810/52711), method 5.25% (1457/27758), class 10.49% (388/3700)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.01% (8889/98635 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.82% (45982/521538), branch 3.45% (1702/49369), complexity 3.43% (1810/52711), method 5.25% (1457/27758), class 10.49% (388/3700)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 207ms / native 294ms = 0.7x speedup
SIMD float-mul (64K x300) java 256ms / native 168ms = 1.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 73.000 ms
Base64 CN1 decode 85.000 ms
Base64 native encode 340.000 ms
Base64 encode ratio (CN1/native) 0.215x (78.5% faster)
Base64 native decode 287.000 ms
Base64 decode ratio (CN1/native) 0.296x (70.4% faster)
Image encode benchmark status skipped (SIMD unsupported)

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 313 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 55ms / native 3ms = 18.3x speedup
SIMD float-mul (64K x300) java 56ms / native 3ms = 18.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 183.000 ms
Base64 CN1 decode 100.000 ms
Base64 native encode 643.000 ms
Base64 encode ratio (CN1/native) 0.285x (71.5% faster)
Base64 native decode 227.000 ms
Base64 decode ratio (CN1/native) 0.441x (55.9% faster)
Base64 SIMD encode 56.000 ms
Base64 encode ratio (SIMD/CN1) 0.306x (69.4% faster)
Base64 SIMD decode 46.000 ms
Base64 decode ratio (SIMD/CN1) 0.460x (54.0% faster)
Base64 encode ratio (SIMD/native) 0.087x (91.3% faster)
Base64 decode ratio (SIMD/native) 0.203x (79.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.286x (71.4% faster)
Image applyMask (SIMD off) 77.000 ms
Image applyMask (SIMD on) 42.000 ms
Image applyMask ratio (SIMD on/off) 0.545x (45.5% faster)
Image modifyAlpha (SIMD off) 79.000 ms
Image modifyAlpha (SIMD on) 83.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.051x (5.1% slower)
Image modifyAlpha removeColor (SIMD off) 98.000 ms
Image modifyAlpha removeColor (SIMD on) 85.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.867x (13.3% faster)

@shai-almog

shai-almog commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog

shai-almog commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1801 seconds

Build and Run Timing

Metric Duration
Simulator Boot 102000 ms
Simulator Boot (Run) 1000 ms
App Install 18000 ms
App Launch 4000 ms
Test Execution 609000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 124ms / native 3ms = 41.3x speedup
SIMD float-mul (64K x300) java 105ms / native 3ms = 35.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 1278.000 ms
Base64 CN1 decode 316.000 ms
Base64 native encode 3378.000 ms
Base64 encode ratio (CN1/native) 0.378x (62.2% faster)
Base64 native decode 1422.000 ms
Base64 decode ratio (CN1/native) 0.222x (77.8% faster)
Base64 SIMD encode 209.000 ms
Base64 encode ratio (SIMD/CN1) 0.164x (83.6% faster)
Base64 SIMD decode 75.000 ms
Base64 decode ratio (SIMD/CN1) 0.237x (76.3% faster)
Base64 encode ratio (SIMD/native) 0.062x (93.8% faster)
Base64 decode ratio (SIMD/native) 0.053x (94.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 14.000 ms
Image createMask (SIMD on) 73.000 ms
Image createMask ratio (SIMD on/off) 5.214x (421.4% slower)
Image applyMask (SIMD off) 281.000 ms
Image applyMask (SIMD on) 274.000 ms
Image applyMask ratio (SIMD on/off) 0.975x (2.5% faster)
Image modifyAlpha (SIMD off) 432.000 ms
Image modifyAlpha (SIMD on) 296.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.685x (31.5% faster)
Image modifyAlpha removeColor (SIMD off) 159.000 ms
Image modifyAlpha removeColor (SIMD on) 299.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.881x (88.1% slower)

@shai-almog

shai-almog commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@shai-almog

shai-almog commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1501 seconds

Build and Run Timing

Metric Duration
Simulator Boot 66000 ms
Simulator Boot (Run) 1000 ms
App Install 22000 ms
App Launch 3000 ms
Test Execution 668000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300) java 71ms / native 3ms = 23.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 361.000 ms
Base64 CN1 decode 147.000 ms
Base64 native encode 763.000 ms
Base64 encode ratio (CN1/native) 0.473x (52.7% faster)
Base64 native decode 539.000 ms
Base64 decode ratio (CN1/native) 0.273x (72.7% faster)
Base64 SIMD encode 151.000 ms
Base64 encode ratio (SIMD/CN1) 0.418x (58.2% faster)
Base64 SIMD decode 72.000 ms
Base64 decode ratio (SIMD/CN1) 0.490x (51.0% faster)
Base64 encode ratio (SIMD/native) 0.198x (80.2% faster)
Base64 decode ratio (SIMD/native) 0.134x (86.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 11.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.364x (63.6% faster)
Image applyMask (SIMD off) 315.000 ms
Image applyMask (SIMD on) 316.000 ms
Image applyMask ratio (SIMD on/off) 1.003x (0.3% slower)
Image modifyAlpha (SIMD off) 186.000 ms
Image modifyAlpha (SIMD on) 265.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.425x (42.5% slower)
Image modifyAlpha removeColor (SIMD off) 121.000 ms
Image modifyAlpha removeColor (SIMD on) 79.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.653x (34.7% faster)

Mirrors the cloud builder fix. Past the frameworks problem the archive
dies in the app's own target with "Embedded binary's bundle identifier
is not prefixed with the parent app's bundle identifier -- Embedded
Binary Bundle Identifier: (null)". Not prefixed wrongly: absent. A modern
Xcode target keeps CFBundleIdentifier and the version strings in build
settings and generates them into the plist, so an extension folder
exported from such a project ships an Info.plist without those keys, and
builtin-infoPlistUtility only expands $(...) references that are already
there -- it does not add the key.

Every extension the builder generates itself writes CFBundleIdentifier =
$(PRODUCT_BUNDLE_IDENTIFIER) into its plist; the generic .ios.appext path
trusted whatever the archive carried. It now adds the same reference when
the key is missing and aligns CFBundleShortVersionString /
CFBundleVersion with the app, which Apple requires of an embedded
extension. A correct value, and one written as a $(...) reference, are
left alone; anything changed is logged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee7eb951e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Mirrors the cloud builder fix. The archive was unpacked into dist/ and
deleted from the resources directory at that point -- after the
translator has already copied that directory into <main>-src, where
every file becomes an app resource. An unbuilt second copy of the
extension therefore rode along inside the .app beside the .appex it was
unpacked into, adding its weight to the IPA and putting the extension's
sources in the bundle.

Unpacking cannot move earlier: it wires Xcode targets, and the project
does not exist yet. The move out of resDir can, and that is all that was
needed -- every other archive kind consumed out of resDir (.lproj.zip,
.placeindist.zip, .framework.zip) deletes itself in the same early pass
for exactly this reason. The archives are now staged into tmp/appext
before the resources are walked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 687ec047e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 4 commits August 21, 2026 12:29
Mirrors the cloud builder fix. The stamper took buildVersion and the
ios.bundleVersion hint, but the app's own Info.plist does not:
ios.plistInject wins for CFBundleShortVersionString and CFBundleVersion,
and the injection only falls back to buildVersion when it says nothing.
An app that injects its version ships that value, while the extension was
stamped with the raw one -- so an extension whose version already MATCHED
its app could be rewritten into one that does not, which is the
embedded-bundle validation failure the stamping exists to prevent.

The Matter extension already resolved both keys correctly, inline. That
resolution is now two named helpers shared by both callers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix for two review catches, both cases where
the stamping edits the wrong thing.

INFOPLIST_FILE: an archive's buildSettings.properties may point the
target at a plist other than <folder>/Info.plist, and those properties
are folded into the target's build settings further down, so Xcode
processes that file into the .appex. Stamping the default left the plist
that actually ships without the identifier and versions. The effective
path is now resolved first (relative to the project directory, with
$(SRCROOT) and $(PROJECT_DIR) understood); a reference this build cannot
resolve returns null and the build says so rather than editing a file
nothing reads.

The value scan: indexOf("<string>") from the key found the next string
ANYWHERE after it, so a CFBundleVersion given <integer>7</integer>, or
the valid empty form <string/>, sent the rewrite into an unrelated later
value -- CFBundleName, or a field inside the NSExtension dict, stamped
with a version number. This is the trap injectedPlistString's comment
records, so the fix is its machinery: the key's own value element, with
whitespace, comments and CDATA skipped. A non-string value is left
alone; <string/> is this key's own empty value and gets filled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. The key lookup was a whole-file search, so
a key of the same name inside a nested dictionary -- NSExtensionAttributes,
CFBundleURLTypes and CFBundleDocumentTypes all carry dictionaries of their
own -- answered first when it came earlier in the file. That reads as "the
bundle already has an identifier" while the real key is still missing, or
sends the version rewrite into an unrelated nested value. The three keys
are now looked up among the DIRECT children of the root dict: a small
walker that steps over each value whole (depth counted on the element's
own name) and skips comments and CDATA rather than reading a < inside
either as a tag.

And CFBundleIdentifier is filled when it is present but empty. It is set
with overwrite off, because an explicit identifier is the extension's own
business -- but <string/> is not an explicit identifier, it is no
identifier, and it fails the embedded-binary check exactly like a missing
one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. Stripping a leading $(SRCROOT)/ handled
the prefix and nothing else, so INFOPLIST_FILE =
$(SRCROOT)/$(TARGET_NAME)/Info.plist -- how an Xcode project writes the
plist that sits in the extension's own folder, i.e. the common case --
was left holding $(TARGET_NAME) and refused as unresolvable, and the
stamper skipped a plist it could have found.

Every setting in that path is known here: SRCROOT and PROJECT_DIR are the
directory the extension folders are extracted into, and TARGET_NAME is
the folder's name, because that is the name the target is created with.
PRODUCT_NAME follows TARGET_NAME unless the archive overrode it with a
literal. Both spellings are substituted, $(NAME) and ${NAME}. A path
still holding a $ afterwards is refused, because a half-resolved path
names some file and editing whichever one it lands on is worse than
saying so. The properties reader is now shared with the other callers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da6420bb2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 4 commits August 21, 2026 13:05
Mirrors the cloud builder fix, and the exemption was wrong in both
directions. A version written as $(MARKETING_VERSION) was left standing on
the grounds that a build-setting reference is the project saying it knows
what it is doing. It is not: the archive's buildSettings.properties are
copied into this target's build configurations further down, so the
reference resolves to whatever they say -- a stale 1.0 under an app at 5.4.
And a reference to a setting they do NOT define resolves to nothing at
all, because the generated target carries no version settings of its own.

A reference is now resolved against those same properties and judged by
the result: one that already lands on the app's version is left alone,
anything else is replaced with the literal, and the log says what it
resolved to. The identifier is untouched by this -- it is written with
overwrite off, so an explicit $(PRODUCT_BUNDLE_IDENTIFIER) still stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. Emptiness was tested on the raw text
between the tags, so <string><!-- note --></string> and
<string><![CDATA[]]></string> counted as values that are already there: a
nonzero run of characters and an empty value. The identifier was then
preserved and the extension shipped without one.

The content is now resolved with plistStringContent first -- CDATA read,
comments stripped, entities decoded -- and emptiness, equality and the
build-setting resolution all run on that, so a version written
<![CDATA[5.4]]> under an app at 5.4 is recognised as already right and
left as the archive wrote it. Padding is not accepted as right: a plist
parser keeps the spaces in <string> 5.4 </string>, so Apple compares
" 5.4 " with the app's "5.4" and rejects the pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. A setting's value may name another setting
and Xcode keeps expanding until none is left; one traversal of the map
does that only when the iteration order happens to be the dependency
order, and Properties hands them over in hash order. With VERSION_SUFFIX
= 1 and MARKETING_VERSION = 5.4$(VERSION_SUFFIX), visiting
MARKETING_VERSION first left $(VERSION_SUFFIX) behind, the strip deleted
it as though nothing defined it, and a version the device resolves to
5.41 read as the app's own 5.4.

Now it expands until nothing changes or nothing is left to expand, capped
so a cycle settles instead of spinning; what survives is treated as
resolving to nothing, which is what Xcode does with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. INFOPLIST_FILE arrives inside the
developer's .ios.appext and the stamper WRITES to whatever it names, so an
absolute path or a ../../ traversal had the builder rewriting a file
outside the project.

The resolved path is now compared, canonically, against the project
directory, and anything landing outside is refused with a log line rather
than edited. Canonical because an archive can carry symlinks: a path that
sits inside the project can still point out of it. The default
<folder>/Info.plist goes through the same check, since a zip may plant a
symlink at that very name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db507a7946

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 5 commits August 21, 2026 13:43
Mirrors the cloud builder comment. Two review rounds proposed the same fix
in different spellings -- that <string /> is not recognised, that
<string>   </string> survives -- and both were already handled, for a
reason that sat one line away and was not written down: plistStringContent
trims on both of its paths, so whitespace, a comment, an empty CDATA
section and any mix arrive as "", and the self-closing test above catches
both <string/> and <string /> because XML puts the slash against the '>'
whatever precedes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n in

Mirrors the cloud builder fix for two review catches.

A qualified setting names a plist too. Xcode honours
INFOPLIST_FILE[sdk=iphoneos*], the archive's properties are copied into
the target verbatim, and a qualified value beats the base one for the
builds it matches, so the device build shipped the one plist the stamper
had not touched. All of them are stamped now; which applies depends on
the sdk, configuration and arch, and stamping is idempotent. Only the
ESCAPED spelling gets this far -- an unescaped one splits on the = inside
the brackets and leaves a key Xcode does not recognise -- so the filter
requires the closing bracket.

And the plist is read as its own bytes declare -- byte order mark first,
then the encoding in its XML declaration -- and written back the same
way. Reading with the platform default charset left a UTF-16 plist as
noise that would not parse, and turned a Latin-1 one's accented
characters into replacements that would then have been written back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. A build that succeeds and an archive that
exports can still be rejected on upload:

    Invalid bundle structure. The ".appex/WalletNonUIExtension" binary
    file is not permitted. Your app cannot contain standalone executables
    or libraries, other than a valid CFBundleExecutable of supported
    bundles.

The .appex never claimed its own binary: its plist has no
CFBundleExecutable, so validation reads the executable inside it as a
loose program rather than the bundle's own. Every extension this builder
generates itself writes CFBundleExecutable, CFBundlePackageType,
CFBundleName, CFBundleInfoDictionaryVersion and CFBundleDevelopmentRegion
into its plist; a brought-in archive that leaves those to
GENERATE_INFOPLIST_FILE arrives without them, so the generic path now
fills the same set when they are missing and keeps whatever the extension
declares itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. CFBundleDevelopmentRegion was filled with
a literal "en", so an extension whose development language is not English
advertised the wrong fallback localization -- and it need not have,
because the archive's buildSettings.properties are copied into this
target, so $(DEVELOPMENT_LANGUAGE) lands on whatever that extension set.
It is also how the builder writes this key for the extensions it
generates itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix. Every generic .ios.appext target was
created with '10.0', hard-coded. Xcode writes that into the .appex as
MinimumOSVersion, so an extension calling iOS 14 APIs shipped claiming
iOS 10 and App Store validation rejected the upload -- "Please ensure the
MinimumOSVersion value of your extension is 14 or later" -- after a build
that succeeded. 10.0 is also below the floor the current SDK builds
against at all.

The target now takes, in order: what the archive's
buildSettings.properties says; 14.0 when its entitlements ask for
payment-pass-provisioning, since PKIssuerProvisioningExtensionHandler is
an iOS 14 API; otherwise the app's own deployment target, never below
12.0. A bare major from the app's hint is normalised to major.minor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a62805064c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Mirrors the cloud builder fix.

The declared deployment target won unconditionally, so an archive
exported from an old project carrying IPHONEOS_DEPLOYMENT_TARGET = 10.0
reproduced the rejection the change exists to prevent. The floor is now a
floor -- 14.0 for a payment-pass extension, 12.0 otherwise -- and the
declared value wins above it.

SWIFT_VERSION: the project's Swift settings are applied to the app target
alone, so a brought-in extension holding .swift reached the compiler with
none and died on "SWIFT_VERSION '' is unsupported", after its sources had
been added to the target. Set from ios.swiftVersion (5.0 by default) with
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES when there is Swift to compile.

TARGETED_DEVICE_FAMILY and SKIP_INSTALL: every generated extension sets
both and the generic path set neither.

And extraction now refuses an archive whose symlinks leave the extension
folder: unzip creates links happily, and everything under that folder is
handed to Xcode, copied into the bundle and swept into the sources
tarball.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the cloud builder fix, found by running the code against a real
customer archive. An extension folder exported from Xcode often carries
two entitlements files -- <Name>.entitlements beside <Name>Release.
entitlements -- and the scan kept overwriting its pick as it walked, so
the file the target is SIGNED with was whichever listFiles() returned
last. The pick is now <ExtensionName>.entitlements when present, else the
first by name, and a folder carrying more than one says so in the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79d1cc668e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Mirrors the cloud builder fixes for the review on this PR.

Insertion point: endOfElement returns the index just PAST the closing
'>', which in a compact plist ending "</dict></plist>" is the '<' of
</plist>, so the inclusive lastIndexOf picked that one and the keys went
between the two closing tags, outside the dict they belong to.

Padding inside markup: <string><![CDATA[ 5.4 ]]></string> parses as
" 5.4 " and was judged on trimmed text, so it counted as matching an app
at "5.4". Emptiness still uses the trimmed value; equality and the
reference resolution use an exact decode, which is what a plist parser
sees. Same for a setting value's own trailing space, which Xcode expands
verbatim.

A stale literal identifier: an archive reused under a renamed package
carries an identifier that is not under the host's bundle id, and
PRODUCT_BUNDLE_IDENTIFIER cannot save it because the literal is what
ships. A literal that could be this app's extension is kept; one that
could not is replaced with the reference.

The entitlements the floor is read from: buildSettings.properties may
point CODE_SIGN_ENTITLEMENTS at another file, and that is the one Xcode
signs against, so that is the one whose payment-pass-provisioning decides
whether the floor is 14.0.

BOM-less UTF-16: legal, and its declaration is NUL-interleaved, so the
ISO-8859-1 probe fell through to UTF-8 and the plist went unstamped. The
first characters of an XML document are "<?", unmistakable either way
round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bdeffa8faf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Mirrors the cloud builder fixes.

An identifier reference is only as good as what it lands on: the archive
may override PRODUCT_BUNDLE_IDENTIFIER with the identifier from the
project the extension was exported from, and that override is written
onto this target, so the ordinary
CFBundleIdentifier=$(PRODUCT_BUNDLE_IDENTIFIER) can still resolve outside
the app. The value is resolved against the settings the target will carry
before it is preserved.

Paths resolve from the settings map, not the properties file: that file
is loaded into the map and deleted before the entitlements lookup runs,
so a CODE_SIGN_ENTITLEMENTS holding $(PRODUCT_NAME) resolved against an
override that was no longer readable and named a different file -- and
the deployment floor was read from entitlements the target is not signed
with.

And a key's own non-string value is replaced rather than left: every key
this stamper manages must be a string, so <integer>7</integer> for
CFBundleVersion is not a version to preserve. Wandering off to a later
key's <string> remains prevented by the anchored lookup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff50725d56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 2 commits August 21, 2026 21:49
…rser

Mirrors the cloud builder fixes for two review catches, both where "we
cannot tell" was being read as "it is fine".

CFBundleIdentifier = $(EXTENSION_BUNDLE_ID) with nothing defining that
setting does not fall back to the target's identifier: Xcode expands it
to the empty string and the .appex ships without one. The reference that
IS safe, $(PRODUCT_BUNDLE_IDENTIFIER), resolves through the settings
because the caller puts the target's own identifier there.

And the deployment floor was decided by searching the entitlements file's
bytes. Entitlements may be UTF-16, where that finds nothing and a Wallet
extension keeps the 12.0 floor Apple rejects it for; and the same text in
a comment or an unrelated value is not a granted entitlement -- taking it
for one drops the extension off every iOS 12 and 13 device. It is read as
a property list now, through the encoding-aware reader, and the key has
to be a top-level <true/>.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every fix in this PR was mirrored from the cloud builder, and every test
stayed behind in it. That is exactly how two copies of the same logic
drift: this one compiles, nothing checks what it does, and the next
divergence is found by a customer.

The six suites are ported to com.codename1.builders, where 41 other
JUnit 4 tests already run under the vintage engine and
AppExtensionResourcesTest already covers the file grouping: the settings
parser, the identity stamping (root-dict anchoring, empty forms, nested
keys, compact plists, references judged by what they resolve to,
non-string values), plist path resolution and confinement, encodings and
qualified INFOPLIST_FILE settings, the deployment floor including the
entitlement parse, and the archive staging and symlink refusal.

70 tests, run against this module's own IPhoneBuilder rather than the
daemon's -- which is the point. They pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de24da07cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…tlements file

Two review catches, mirrored from the cloud builder.

INFOPLIST_FILE = $(PLIST_DIR)/Info.plist, with PLIST_DIR defined two lines
above it in the same buildSettings.properties, is a path Xcode resolves:
both settings are copied onto the target. Expanding only SRCROOT,
PROJECT_DIR, TARGET_NAME and PRODUCT_NAME called it unresolvable and left
the plist Xcode actually builds unstamped. Every declared setting now
expands, to a fixed point, before those four.

And an entitlements file may be binary -- Xcode writes those as readily as
XML and they sign identically, but the XML walk cannot read one, so an
issuer-provisioning extension was handed the 12.0 floor Apple rejects it
for. Binary files go through plutil where there is one, and fall back to a
byte search where there is not: the old behaviour, kept only for the file
kind that has no comments to be fooled by. XML still goes through the
parser.

The two affected suites are re-ported with the new cases; 74 tests run
against this module's own IPhoneBuilder, all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 894b2d2c0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +5816 to +5818
String declaredId = appExtensionBuildSetting(appExtension, "PRODUCT_BUNDLE_IDENTIFIER");
settings.put("PRODUCT_BUNDLE_IDENTIFIER", declaredId != null ? declaredId
: request.getPackageName() + "." + appExtension.getName());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve qualified bundle-identifier overrides

When buildSettings.properties contains a device-qualified override such as PRODUCT_BUNDLE_IDENTIFIER[sdk\=iphoneos*]=com.old.Ext, this lookup sees only the unqualified value and adds the host-derived default, so identifierBelongsToApp() preserves the usual $(PRODUCT_BUNDLE_IDENTIFIER) reference. The later Ruby loop emits both settings, and the qualified value wins for the device archive, leaving the extension outside the containing app's identifier prefix. Fresh evidence in this revision is that qualified settings are explicitly supported for INFOPLIST_FILE and copied verbatim, but the identifier check still consults only the base key.

Useful? React with 👍 / 👎.

Comment on lines +4916 to +4920
String extDeploymentTarget = appExtensionDeploymentTarget(
buildSettingsMap.get("IPHONEOS_DEPLOYMENT_TARGET"),
signedEntitlements,
request.getArg("ios.deployment_target", null));
buildSettingsMap.put("IPHONEOS_DEPLOYMENT_TARGET", extDeploymentTarget);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp qualified deployment-target settings

When an archive supplies IPHONEOS_DEPLOYMENT_TARGET[sdk\=iphoneos*]=10.0, this calculation clamps only the unqualified setting to 12.0 or 14.0 while leaving the qualified entry in buildSettingsMap. The Ruby loop subsequently writes both entries to the target, so the device build selects 10.0 and bypasses the newly calculated floor, which can again fail SDK or Wallet-extension validation. Fresh evidence in this revision is the new explicit handling of qualified plist settings, confirming that conditional settings are supported by these imported archives even though the deployment-floor logic ignores them.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant