Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
bc79d43
Give an app extension its build settings, so UIKit links
shai-almog Aug 21, 2026
ee7eb95
Give a brought-in app extension a bundle identity
shai-almog Aug 21, 2026
687ec04
Stop shipping the .ios.appext archive inside the app
shai-almog Aug 21, 2026
115bf16
Resolve extension versions the way the app resolves its own
shai-almog Aug 21, 2026
0cff60f
Stamp the plist the target builds, and only the key's own value
shai-almog Aug 21, 2026
f225368
Read the bundle's identity off the root dict, and fill an empty one
shai-almog Aug 21, 2026
da6420b
Resolve the build settings the plist path is actually written with
shai-almog Aug 21, 2026
2f01b91
Judge a version reference by what it resolves to
shai-almog Aug 21, 2026
d7f1121
Read an identifier's value, not its spelling
shai-almog Aug 21, 2026
b569dbc
Expand a build setting to a fixed point, not one pass
shai-almog Aug 21, 2026
db507a7
Refuse to stamp a plist outside the project directory
shai-almog Aug 21, 2026
886e779
Say at the check why every empty spelling is already empty
shai-almog Aug 21, 2026
cf156d4
Stamp every plist the target may build, in the encoding it was writte…
shai-almog Aug 21, 2026
2b4ae20
Declare the extension's binary, so the IPA is a valid bundle
shai-almog Aug 21, 2026
98c2b5e
Take the development region from the extension, not from us
shai-almog Aug 21, 2026
a628050
Give a brought-in extension a minimum iOS it can ship with
shai-almog Aug 21, 2026
c487df6
Enforce the floor, and close the rest of the parity gaps
shai-almog Aug 21, 2026
79d1cc6
Pick the entitlements by name, not by directory order
shai-almog Aug 21, 2026
bdeffa8
Six review catches on the plist stamper
shai-almog Aug 21, 2026
ff50725
Three more review catches on the stamper
shai-almog Aug 21, 2026
d8144e4
An unknown reference is not an identifier, and grep is not a plist pa…
shai-almog Aug 21, 2026
de24da0
Test the builder here too, not only in the cloud daemon
shai-almog Aug 21, 2026
894b2d2
Expand the archive's own settings in a plist path; read a binary enti…
shai-almog Aug 22, 2026
441081b
Clamp the conditional settings, not just the plain ones
shai-almog Aug 22, 2026
4f527a2
Refuse an extension identifier outside the app, don't just say so
shai-almog Aug 22, 2026
5c674a6
Judge a setting by what it resolves to, before clamping or refusing it
shai-almog Aug 22, 2026
0f4463f
Resolve through Xcode's own settings, and read every entitlements file
shai-almog Aug 22, 2026
e87bb8d
Expand a PRODUCT_NAME chain; ignore conditions this archive never uses
shai-almog Aug 22, 2026
d20771c
A qualified setting overrides the plain one; it does not add to it
shai-almog Aug 22, 2026
52a2f05
Match a condition against the SDK and arch this archive really uses
shai-almog Aug 22, 2026
6d2099e
Resolve with the archive's own context, and stop assuming an SDK version
shai-almog Aug 22, 2026
6e4f152
An unwildcarded sdk condition matches exactly, as Xcode matches it
shai-almog Aug 22, 2026
5288b0e
Ask the selected Xcode for its SDK, and fill the context first
shai-almog Aug 22, 2026
02dfe48
Take the SDK from Xcode, and repair only this archive's settings
shai-almog Aug 23, 2026
da061b5
Three review catches: modifiers, the shared base target, and a cycle
shai-almog Aug 23, 2026
a76dc86
Rank an exact condition above a wildcard; take the arch from the build
shai-almog Aug 23, 2026
dd5d465
Rank wildcards by width, refuse a foreign variant, clamp an empty target
shai-almog Aug 23, 2026
8f79057
The archive's variant is the archive's to declare, and plist paths re…
shai-almog Aug 23, 2026
e6d0a15
Give repairs the whole context, and resolve a qualifier where it lives
shai-almog Aug 23, 2026
c587b75
Resolve conditionals before expanding, and three smaller corrections
shai-almog Aug 23, 2026
3c6c724
Three review catches: padding, helper settings, and variant chains
shai-almog Aug 23, 2026
153c897
Flatten before resolving, and pick the variant list for the archive
shai-almog Aug 23, 2026
b90cb2d
Mirror: one settings reader, and device families from the project type
shai-almog Aug 23, 2026
4638802
Mirror: each Info.plist is stamped in its own candidate's context
shai-almog Aug 23, 2026
ec48ae4
Mirror: the containing app's own plist is never stamped
shai-almog Aug 23, 2026
3090361
Mirror: supply $(PROJECT_NAME), keep a wildcard a wildcard
shai-almog Aug 23, 2026
0c88b83
Mirror: every qualifier group reaches the candidate's context
shai-almog Aug 23, 2026
3cf35f9
Mirror: one plist named by two conditions is stamped in both
shai-almog Aug 23, 2026
e59712b
Empty is a value, and / is not a developer directory
shai-almog Aug 23, 2026
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,11 @@ static java.util.List<String> injectedPlistStringArray(BuildRequest request, Str

/// Where the value belonging to {@code key} begins -- just past its {@code </key>} -- or -1
/// when the fragment does not carry the key.
private static int injectedValueAt(String inject, String key) {
/// Shared with IPhoneBuilder's app-extension Info.plist stamping, which has to find a
/// key's own value in a real plist for the same reason the comment on
/// {@link #injectedPlistString} gives: the next `<string>` after a key is very often some
/// other key's.
static int injectedValueAt(String inject, String key) {
int at = 0;
while (true) {
int content = contentAfterOpenTag(inject, "key", at);
Expand All @@ -969,7 +973,7 @@ private static int injectedValueAt(String inject, String key) {
/// The {@code <} of the next real element at or after {@code from}, or -1 when what follows is
/// text or nothing. Whitespace, comments and CDATA sit between a key and its value in real
/// fragments and none of them is the value.
private static int nextElementAt(String inject, int from) {
static int nextElementAt(String inject, int from) {
int i = from;
while (i < inject.length()) {
if (Character.isWhitespace(inject.charAt(i))) {
Expand All @@ -990,7 +994,7 @@ private static int nextElementAt(String inject, int from) {
}

/// The element name at an opening tag, lowercased. Empty for an end tag, which is not one.
private static String tagAt(String inject, int element) {
static String tagAt(String inject, int element) {
StringBuilder tag = new StringBuilder();
for (int j = element + 1; j < inject.length()
&& Character.isLetterOrDigit(inject.charAt(j)); j++) {
Expand Down Expand Up @@ -1074,7 +1078,7 @@ private static int contentAfterOpenTag(String inject, String element, int from)
///
/// Returns `at` when it is already outside both, the position just past the enclosing
/// construct when it is not, and -1 when that construct never ends.
private static int skipMarkupBefore(String inject, int at, int from) {
static int skipMarkupBefore(String inject, int at, int from) {
int cdata = inject.indexOf(CDATA_OPEN, from);
int comment = inject.indexOf(COMMENT_OPEN, from);
boolean cdataFirst = cdata >= 0 && (comment < 0 || cdata < comment);
Expand Down Expand Up @@ -1116,7 +1120,7 @@ private static int nextMarkup(String inject, String tag, int from) {
}

/// The end tag that closes an element, skipping over CDATA sections and comments.
private static int closeOfElement(String inject, int from, String closeTag) {
static int closeOfElement(String inject, int from, String closeTag) {
// `</key >` closes the same element as `</key>`, so the tag is matched as a pattern rather
// than as literal text -- the same reason the opening tags are.
java.util.regex.Matcher m = java.util.regex.Pattern
Expand Down Expand Up @@ -1200,11 +1204,19 @@ private static String stripComments(String value) {
/// entity decoding applies as before. The assembled value is trimmed, matching what this did
/// before CDATA was understood at all.
static String plistStringContent(String raw) {
String exact = plistStringContentExact(raw);
return exact == null ? null : exact.trim();
}

/// The same content WITHOUT the trim, for a caller that has to see the value a plist parser
/// would: <string> 5.4 </string> and <string><![CDATA[ 5.4 ]]></string> both carry padding
/// that Apple compares and this method must not throw away.
static String plistStringContentExact(String raw) {
if (raw == null) {
return null;
}
if (raw.indexOf(CDATA_OPEN) < 0) {
return decodeXmlEntities(stripComments(raw).trim());
return decodeXmlEntities(stripComments(raw));
}
StringBuilder out = new StringBuilder(raw.length());
int i = 0;
Expand All @@ -1225,7 +1237,7 @@ static String plistStringContent(String raw) {
out.append(raw, body, end);
i = end + CDATA_CLOSE.length();
}
return out.toString().trim();
return out.toString();
}

/// Turns the five predefined XML entities back into their characters.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.builders;

import org.junit.Test;

import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class AppExtensionBuildSettingsTest {

/**
* The block is written the way it appears in a pbxproj: tab-indented, one
* {@code KEY = VALUE;} per line.
*/
private static final String BLOCK = "CLANG_ANALYZER_NONNULL = YES;\n"
+ "\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n"
+ "\t\t\t\tCLANG_ENABLE_MODULES = YES;\n"
+ "\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n"
+ "\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;";

@Test
public void valueLosesItsTrailingSemicolon() {
Map<String, String> settings = IPhoneBuilder.parseXcodeBuildSettings(BLOCK);
// A value of ";" is what CLANG_ENABLE_MODULES used to get, and Xcode reads that
// as "off": no -fmodules, no clang autolinking, and an extension importing UIKit
// reaches ld with Foundation alone and fails on _OBJC_CLASS_$_UIView.
assertEquals("YES", settings.get("CLANG_ENABLE_MODULES"));
assertEquals("YES", settings.get("CLANG_ENABLE_OBJC_ARC"));
assertEquals("YES", settings.get("CLANG_ANALYZER_NONNULL"));
assertEquals("YES_AGGRESSIVE", settings.get("CLANG_WARN_UNGUARDED_AVAILABILITY"));
}

@Test
public void quotedValueIsUnwrappedBeforeItBecomesARubyLiteral() {
Map<String, String> settings = IPhoneBuilder.parseXcodeBuildSettings(BLOCK);
// Kept quotes would be emitted as e.build_settings['...'] = ""gnu++14"", which
// is a Ruby syntax error that takes the whole project fixup script with it.
assertEquals("gnu++14", settings.get("CLANG_CXX_LANGUAGE_STANDARD"));
}

@Test
public void blankAndMalformedLinesAreSkipped() {
Map<String, String> settings = IPhoneBuilder.parseXcodeBuildSettings(
"\n \nCLANG_ENABLE_MODULES = YES;\nnot a setting\n");
assertEquals(1, settings.size());
assertEquals("YES", settings.get("CLANG_ENABLE_MODULES"));
}

@Test
public void paddingIsStrippedFromArchiveSettings() throws Exception {
java.io.File dist = java.nio.file.Files.createTempDirectory("appext").toFile();
java.io.File extension = new java.io.File(dist, "WalletUIExtension");
assertTrue(extension.mkdirs());
java.io.FileWriter w = new java.io.FileWriter(
new java.io.File(extension, "buildSettings.properties"));
w.write("PRODUCT_BUNDLE_IDENTIFIER=com.example.app.Ext \n");
w.close();

// Properties keeps the trailing space and Xcode does not. Kept, preflight validated
// "com.example.app.Ext" while the target was handed "com.example.app.Ext " -- an
// identifier no profile matches, from two readers of one file disagreeing.
assertEquals("com.example.app.Ext", IPhoneBuilder.appExtensionBuildSettings(extension)
.get("PRODUCT_BUNDLE_IDENTIFIER"));
}

@Test
public void extensionDeviceFamiliesFollowTheApp() {
// The translator gives the app target "1" for iphone and "2" for anything else that is
// not "ios"; an extension pinned to "1,2" beside an iPhone-only app is an upload
// rejection for an embedded bundle its container does not support.
assertEquals("1", IPhoneBuilder.embeddedExtensionDeviceFamily("iphone"));
assertEquals("2", IPhoneBuilder.embeddedExtensionDeviceFamily("ipad"));
assertEquals("1,2", IPhoneBuilder.embeddedExtensionDeviceFamily("ios"));
assertEquals("1,2", IPhoneBuilder.embeddedExtensionDeviceFamily(null));
}
}
Loading