From 9fa6f83c46ac260e04b67036dd23e6e3ec85734f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:16:25 +0200 Subject: [PATCH 1/3] Adding Swift Package Manager support due to CocoaPods deprecation --- .github/workflows/ios-packaging.yml | 138 +++++++++++++ .../Advanced-Topics-Under-The-Hood.asciidoc | 2 +- .../developer-guide/Working-With-iOS.asciidoc | 18 ++ docs/website/content/blog/cocoapods.md | 2 + ...-ios-cocoapods-dependencies-native-code.md | 2 + ...-now-build-android-and-ios-apps-locally.md | 2 +- .../builders/IOSDependencyManager.java | 172 +++++++++++++++++ .../com/codename1/builders/IPhoneBuilder.java | 182 +++++++++++++++--- .../com/codename1/maven/CN1BuildMojo.java | 22 ++- .../IPhoneBuilderDependencyConfigTest.java | 156 +++++++++++++++ scripts/build-ios-app.sh | 43 +++-- scripts/run-ios-native-tests.sh | 19 +- scripts/run-ios-ui-tests.sh | 26 ++- 13 files changed, 733 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/ios-packaging.yml create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IOSDependencyManager.java create mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/IPhoneBuilderDependencyConfigTest.java diff --git a/.github/workflows/ios-packaging.yml b/.github/workflows/ios-packaging.yml new file mode 100644 index 0000000000..0c7ef98b1e --- /dev/null +++ b/.github/workflows/ios-packaging.yml @@ -0,0 +1,138 @@ +name: Test iOS packaging + +on: + pull_request: + paths: + - '.github/workflows/ios-packaging.yml' + - 'maven/codenameone-maven-plugin/**' + - 'vm/ByteCodeTranslator/**' + - 'scripts/build-ios-app.sh' + - 'scripts/run-ios-ui-tests.sh' + - 'scripts/run-ios-native-tests.sh' + - 'scripts/ios/**' + - 'scripts/hellocodenameone/**' + - '!docs/**' + push: + branches: [ master ] + paths: + - '.github/workflows/ios-packaging.yml' + - 'maven/codenameone-maven-plugin/**' + - 'vm/ByteCodeTranslator/**' + - 'scripts/build-ios-app.sh' + - 'scripts/run-ios-ui-tests.sh' + - 'scripts/run-ios-native-tests.sh' + - 'scripts/ios/**' + - 'scripts/hellocodenameone/**' + - '!docs/**' + +jobs: + packaging-matrix: + permissions: + contents: read + runs-on: macos-15 + timeout-minutes: 75 + strategy: + fail-fast: false + matrix: + packaging: + - name: pods-only + args: >- + -Dcodename1.arg.ios.dependencyManager=cocoapods + -Dcodename1.arg.ios.pods=AFNetworking + - name: spm-only + args: >- + -Dcodename1.arg.ios.dependencyManager=spm + -Dcodename1.arg.ios.spm.packages=swift-collections|https://github.com/apple/swift-collections.git|from:1.1.0 + -Dcodename1.arg.ios.spm.products.swift-collections=Collections + - name: both + args: >- + -Dcodename1.arg.ios.dependencyManager=both + -Dcodename1.arg.ios.pods=AFNetworking + -Dcodename1.arg.ios.spm.packages=swift-collections|https://github.com/apple/swift-collections.git|from:1.1.0 + -Dcodename1.arg.ios.spm.products.swift-collections=Collections + + steps: + - uses: actions/checkout@v4 + + - name: Ensure CocoaPods tooling + run: | + mkdir -p ~/.codenameone + cp maven/UpdateCodenameOne.jar ~/.codenameone/ + set -euo pipefail + GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')" + export PATH="$GEM_USER_DIR/bin:$PATH" + gem install cocoapods xcodeproj --no-document --user-install + pod --version + + - name: Compute setup-workspace hash + id: setup_hash + run: | + set -euo pipefail + echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT" + + - name: Set TMPDIR + run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV + + - name: Cache codenameone-tools + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/codenameone-tools + key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + ${{ runner.os }}-cn1-tools- + + - name: Restore cn1-binaries cache + uses: actions/cache@v4 + with: + path: ../cn1-binaries + key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + cn1-binaries-${{ runner.os }}- + + - name: Setup workspace + run: ./scripts/setup-workspace.sh -q -DskipTests + timeout-minutes: 25 + + - name: Build iOS port + run: ./scripts/build-ios-port.sh -q -DskipTests + timeout-minutes: 25 + + - name: Build sample iOS app + id: build_ios_app + env: + IOS_DEPENDENCY_ARGS: ${{ matrix.packaging.args }} + run: ./scripts/build-ios-app.sh -q -DskipTests + timeout-minutes: 30 + + - name: Run iOS UI smoke + env: + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/${{ matrix.packaging.name }} + run: | + set -euo pipefail + mkdir -p "${ARTIFACTS_DIR}" + ./scripts/run-ios-ui-tests.sh \ + "${{ steps.build_ios_app.outputs.workspace }}" \ + "" \ + "${{ steps.build_ios_app.outputs.scheme }}" + timeout-minutes: 30 + + - name: Run native iOS notification tests + if: matrix.packaging.name == 'both' + env: + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/${{ matrix.packaging.name }}-native + run: | + set -euo pipefail + mkdir -p "${ARTIFACTS_DIR}" + ./scripts/run-ios-native-tests.sh \ + "${{ steps.build_ios_app.outputs.workspace }}" \ + "${{ steps.build_ios_app.outputs.scheme }}" + timeout-minutes: 20 + + - name: Upload packaging artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-packaging-${{ matrix.packaging.name }} + path: artifacts + if-no-files-found: warn + retention-days: 14 diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index b973a9b8e8..2304ca038d 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -645,7 +645,7 @@ Codename One supports debugging applications on devices by using the natively ge In iOS this is usually strait forward, just open the project with xcode and run it optionally disabling bitcode. Unzip the .bz2 file and open the `.xcworkspace` file if it's available otherwise open the `.xcodeproj` file inside the `dist` directory. -IMPORTANT: Only the `.xcworkspace` if it is there, it is activated by the CocoaPods build pipeline so it won't always be there +IMPORTANT: The `.xcworkspace` is no longer exclusive to CocoaPods-based builds. Use it whenever it is generated, whether the project uses CocoaPods, Swift Package Manager, or both. With Android Studio this is sometimes as very easy task as it is possible to actually open the gradle project in Android Studio and just run it. However, due to the fragile nature of the gradle project this stopped working for some builds and has been "flaky". diff --git a/docs/developer-guide/Working-With-iOS.asciidoc b/docs/developer-guide/Working-With-iOS.asciidoc index 5b1ee8b232..197b32e541 100644 --- a/docs/developer-guide/Working-With-iOS.asciidoc +++ b/docs/developer-guide/Working-With-iOS.asciidoc @@ -192,6 +192,8 @@ However, it seems that Apple will reject your app if you just include that and d === Using Cocoapods +NOTE: CocoaPods remains fully supported, but it is no longer the only supported iOS dependency path. Swift Package Manager (SPM) is also supported. The current guidance is documented in this section. + https://cocoapods.org/[CocoaPods] is a dependency manager for Swift and Objective-C Cocoa projects. It has over eighteen thousand libraries and can help you scale your projects elegantly. Cocoapods can be used in your Codename One project to include native iOS libraries without having to go through the hassle of bundling the actual library into your project. Rather than bundling .h and .a files in your ios/native directory, you can specify which "pods" your app uses via the `ios.pods` build hint. (There are other build hints also if you need more advanced features). **Examples** @@ -252,6 +254,22 @@ ios.pods=GoogleMaps (Note that the `ios.pods.sources` directive is optional). +=== Using Swift Package Manager + +Swift Package Manager can be used as an alternative to CocoaPods for remote Swift package dependencies on iOS. Select the dependency path with `ios.dependencyManager`. Supported values are `auto`, `cocoapods`, `spm`, and `both`. + +For an SPM-only configuration, declare the packages in `ios.spm.packages` using the format `||` and then declare the products to link using `ios.spm.products.`. + +---- +ios.dependencyManager=spm +ios.spm.packages=swift-collections|https://github.com/apple/swift-collections.git|from:1.1.0 +ios.spm.products.swift-collections=Collections +---- + +Supported requirement formats are `from:`, `exact:`, `branch:`, `revision:`, and `range:`. + +`ios.dependencyManager=auto` preserves backward compatibility. Existing projects with only `ios.pods` continue to use CocoaPods. Projects with only `ios.spm.*` use SPM. If both hint families are present, both are applied. + === Including Dynamic Frameworks If you need to use a dynamic framework (e.g. SomeThirdPartySDK.framework), and it isn't available via cocoapods, then you can add it to your project by simply zipping up the framework and copying it to your native/ios directory. diff --git a/docs/website/content/blog/cocoapods.md b/docs/website/content/blog/cocoapods.md index 7a6ccd6d24..bad4077b5e 100644 --- a/docs/website/content/blog/cocoapods.md +++ b/docs/website/content/blog/cocoapods.md @@ -11,6 +11,8 @@ author: Steve Hannah ![Header Image](/blog/cocoapods/cocoapods.png) +_Editor note: CocoaPods remains supported, but current iOS dependency guidance now also covers Swift Package Manager (SPM) and mixed CocoaPods/SPM setups. See the current "Working with iOS" section in the developer guide._ + [CocoaPods](https://cocoapods.org/) is a dependency manager for Swift and Objective-C Cocoa projects. It has over eighteen thousand libraries and can help you scale your projects elegantly. Cocoapods can be used in your Codename One project to include native iOS libraries without having to go through the hassle diff --git a/docs/website/content/blog/tip-use-ios-cocoapods-dependencies-native-code.md b/docs/website/content/blog/tip-use-ios-cocoapods-dependencies-native-code.md index e8189085a1..c6ab116e81 100644 --- a/docs/website/content/blog/tip-use-ios-cocoapods-dependencies-native-code.md +++ b/docs/website/content/blog/tip-use-ios-cocoapods-dependencies-native-code.md @@ -11,6 +11,8 @@ author: Shai Almog ![Header Image](/blog/tip-use-ios-cocoapods-dependencies-native-code/tip.jpg) +_Editor note: This post is still valid for CocoaPods, but current iOS dependency setup also supports Swift Package Manager (SPM). See the current "Working with iOS" section in the developer guide._ + Last week I talked about [using gradle dependencies](/blog/tip-use-android-gradle-dependencies-native-code.html) to build native code, this week I’ll talk about the iOS equivalent: CocoaPods. We’ve [discussed CocoaPods before](/blog/cocoapods.html) but this bares repeating especially in the context of a specific cn1lib like [intercom](/blog/intercom-support.html). CocoaPods allow us to add a native library dependency to iOS far more easily than Gradle. However, I did run into a caveat with target OS versioning. By default we target iOS 7.0 or newer which is supported by Intercom only for older versions of the library. Annoyingly CocoaPods seemed to work, to solve this we had to explicitly define the build hint `ios.pods.platform=8.0` to force iOS 8 or newer. diff --git a/docs/website/content/blog/you-can-now-build-android-and-ios-apps-locally.md b/docs/website/content/blog/you-can-now-build-android-and-ios-apps-locally.md index c9a84cb59a..ef9676a424 100644 --- a/docs/website/content/blog/you-can-now-build-android-and-ios-apps-locally.md +++ b/docs/website/content/blog/you-can-now-build-android-and-ios-apps-locally.md @@ -28,7 +28,7 @@ These are unresolved questions, but we are poised to find out their answers, as ### iOS Builds -The Local iOS build target generates an Xcode project that you can open and build directly in Xcode. This target necessarily requires a Mac with Xcode and cocoapods installed. See [Building for iOS](https://shannah.github.io/cn1-maven-archetypes/cn1app-archetype-tutorial/getting-started.html#ios) from [this tutorial](https://shannah.github.io/cn1-maven-archetypes/cn1app-archetype-tutorial/getting-started.html) for more information. +The Local iOS build target generates an Xcode project that you can open and build directly in Xcode. This target requires a Mac with Xcode installed. CocoaPods is only required for CocoaPods-based builds; projects that use Swift Package Manager use the normal Apple toolchain instead. See the current "Working with iOS" section in the developer guide and [Building for iOS](https://shannah.github.io/cn1-maven-archetypes/cn1app-archetype-tutorial/getting-started.html#ios) for more information. ![intellij-build-ios-project](https://www.codenameone.com/wp-content/uploads/2021/04/intellij-build-ios-project.png) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IOSDependencyManager.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IOSDependencyManager.java new file mode 100644 index 0000000000..ecf625b054 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IOSDependencyManager.java @@ -0,0 +1,172 @@ +package com.codename1.builders; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public enum IOSDependencyManager { + AUTO, + COCOAPODS, + SPM, + BOTH, + NONE; + + static IOSDependencyConfig resolve(BuildRequest request, String iosPods) throws BuildException { + IOSDependencyManager explicitMode = fromHint(request.getArg("ios.dependencyManager", "auto")); + List swiftPackages = SwiftPackageSpec.parse(request); + return explicitMode.resolve(iosPods, swiftPackages); + } + + static IOSDependencyManager fromHint(String value) throws BuildException { + String normalized = value == null ? "auto" : value.trim().toLowerCase(Locale.ROOT); + if (normalized.length() == 0) { + normalized = "auto"; + } + switch (normalized) { + case "auto": + return AUTO; + case "cocoapods": + return COCOAPODS; + case "spm": + return SPM; + case "both": + return BOTH; + case "none": + return NONE; + default: + throw new BuildException("Unsupported ios.dependencyManager value " + value + ". Expected one of auto, cocoapods, spm, both, none"); + } + } + + IOSDependencyConfig resolve(String iosPods, List swiftPackages) throws BuildException { + boolean hasPods = iosPods != null && iosPods.trim().length() > 0; + boolean hasSpm = swiftPackages != null && !swiftPackages.isEmpty(); + switch (this) { + case AUTO: + if (hasPods && hasSpm) { + return new IOSDependencyConfig(BOTH, iosPods, swiftPackages); + } + if (hasPods) { + return new IOSDependencyConfig(COCOAPODS, iosPods, swiftPackages); + } + if (hasSpm) { + return new IOSDependencyConfig(SPM, iosPods, swiftPackages); + } + return new IOSDependencyConfig(NONE, iosPods, swiftPackages); + case COCOAPODS: + if (!hasPods) { + throw new BuildException("ios.dependencyManager=cocoapods requires ios.pods to be set"); + } + return new IOSDependencyConfig(COCOAPODS, iosPods, swiftPackages); + case SPM: + if (!hasSpm) { + throw new BuildException("ios.dependencyManager=spm requires ios.spm.packages to be set"); + } + return new IOSDependencyConfig(SPM, iosPods, swiftPackages); + case BOTH: + if (!hasPods || !hasSpm) { + throw new BuildException("ios.dependencyManager=both requires both ios.pods and ios.spm.packages to be set"); + } + return new IOSDependencyConfig(BOTH, iosPods, swiftPackages); + case NONE: + return new IOSDependencyConfig(NONE, iosPods, swiftPackages); + default: + throw new BuildException("Unsupported ios.dependencyManager value " + this); + } + } +} + +final class IOSDependencyConfig { + final IOSDependencyManager mode; + final String iosPods; + final List swiftPackages; + + IOSDependencyConfig(IOSDependencyManager mode, String iosPods, List swiftPackages) { + this.mode = mode; + this.iosPods = iosPods == null ? "" : iosPods.trim(); + this.swiftPackages = swiftPackages; + } + + boolean usesCocoaPods() { + return mode == IOSDependencyManager.COCOAPODS || mode == IOSDependencyManager.BOTH; + } + + boolean usesSwiftPackages() { + return mode == IOSDependencyManager.SPM || mode == IOSDependencyManager.BOTH; + } +} + +final class SwiftPackageSpec { + final String identity; + final String url; + final String requirement; + final List products; + + SwiftPackageSpec(String identity, String url, String requirement, List products) { + this.identity = identity; + this.url = url; + this.requirement = requirement; + this.products = products; + } + + static List parse(BuildRequest request) throws BuildException { + String packagesProp = request.getArg("ios.spm.packages", ""); + List out = new ArrayList(); + if (packagesProp == null || packagesProp.trim().length() == 0) { + return out; + } + String[] packageEntries = packagesProp.split("[;]"); + for (String entry : packageEntries) { + String trimmed = entry.trim(); + if (trimmed.length() == 0) { + continue; + } + String[] parts = trimmed.split("\\|"); + if (parts.length != 3) { + throw new BuildException("Invalid ios.spm.packages entry '" + trimmed + "'. Expected ||"); + } + String identity = parts[0].trim(); + String url = parts[1].trim(); + String requirement = parts[2].trim(); + if (identity.length() == 0 || url.length() == 0 || requirement.length() == 0) { + throw new BuildException("Invalid ios.spm.packages entry '" + trimmed + "'. Identity, URL, and requirement are all required"); + } + validateRequirement(requirement); + String productsProp = request.getArg("ios.spm.products." + identity, ""); + List products = new ArrayList(); + for (String product : productsProp.split("[,]")) { + product = product.trim(); + if (product.length() > 0) { + products.add(product); + } + } + if (products.isEmpty()) { + throw new BuildException("ios.spm.products." + identity + " must list at least one product"); + } + out.add(new SwiftPackageSpec(identity, url, requirement, products)); + } + return out; + } + + static void validateRequirement(String requirement) throws BuildException { + if (requirement.startsWith("from:") || requirement.startsWith("exact:") || + requirement.startsWith("branch:") || requirement.startsWith("revision:")) { + if (requirement.substring(requirement.indexOf(':') + 1).trim().length() == 0) { + throw new BuildException("Invalid SPM requirement '" + requirement + "'"); + } + return; + } + if (requirement.startsWith("range:")) { + String range = requirement.substring("range:".length()).trim(); + if (!range.contains("..<")) { + throw new BuildException("Invalid SPM range requirement '" + requirement + "'. Expected range:min.. 0; if (usePodsForFacebook) { String fbPodsVersion = request.getArg("ios.facebook.version", "~>5.6.0"); addMinDeploymentTarget("10.0"); iosPods += (((iosPods.length() > 0) ? ",":"") + "FBSDKCoreKit "+fbPodsVersion+",FBSDKLoginKit "+fbPodsVersion+",FBSDKShareKit "+fbPodsVersion); } - - runPods = true; - - + String googleAdUnitId = request.getArg("ios.googleAdUnitId", request.getArg("google.adUnitId", null)); - boolean usePodsForGoogleAds = runPods && googleAdUnitId != null && googleAdUnitId.length() > 0; + boolean usePodsForGoogleAds = googleAdUnitId != null && googleAdUnitId.length() > 0; if (usePodsForGoogleAds) { iosPods += (((iosPods.length() > 0) ? ",":"") + "Firebase/Core,Firebase/AdMob"); addMinDeploymentTarget("7.0"); @@ -326,6 +338,17 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException addMinDeploymentTarget("8.0"); } + IOSDependencyConfig dependencyConfig = IOSDependencyManager.resolve(request, iosPods); + iosPods = dependencyConfig.iosPods; + runPods = dependencyConfig.usesCocoaPods(); + runSpm = dependencyConfig.usesSwiftPackages(); + if (runPods) { + ensurePodsInstalled(); + } + if (runSpm) { + ensureXcodeprojInstalled(); + } + debug("Xcode version is "+xcodeVersion); String iosMode = request.getArg("ios.themeMode", "auto"); @@ -1980,6 +2003,14 @@ public void usesClassMethod(String cls, String method) { stopwatch.split("CocoaPods"); } + if (runSpm) { + configureSwiftPackages(request, dependencyConfig); + if (!runPods) { + ensureTopLevelWorkspace(request); + } + stopwatch.split("SwiftPM"); + } + try { @@ -2042,6 +2073,111 @@ public File getXcodeProjectDir() { return xcodeProjectDir; } + private void configureSwiftPackages(BuildRequest request, IOSDependencyConfig dependencyConfig) throws BuildException { + if (!dependencyConfig.usesSwiftPackages()) { + return; + } + File hooksDir = new File(tmpFile, "hooks"); + hooksDir.mkdir(); + File configFile = new File(hooksDir, "configure_swift_packages.rb"); + StringBuilder script = new StringBuilder(); + script.append("#!/usr/bin/env ruby\n") + .append("require 'xcodeproj'\n") + .append("project_file = '").append(escapeRuby(new File(tmpFile, "dist/" + request.getMainClass() + ".xcodeproj").getAbsolutePath())).append("'\n") + .append("xcproj = Xcodeproj::Project.open(project_file)\n") + .append("target = xcproj.targets.find { |t| t.name == '").append(escapeRuby(request.getMainClass())).append("' }\n") + .append("abort('Unable to find app target ").append(escapeRuby(request.getMainClass())).append("') unless target\n"); + for (SwiftPackageSpec spec : dependencyConfig.swiftPackages) { + script.append("package_ref = xcproj.root_object.package_references.find { |pkg| pkg.respond_to?(:repositoryURL) && pkg.repositoryURL == '") + .append(escapeRuby(spec.url)).append("' }\n") + .append("if package_ref.nil?\n") + .append(" package_ref = xcproj.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)\n") + .append(" package_ref.repositoryURL = '").append(escapeRuby(spec.url)).append("'\n") + .append(" package_ref.requirement = ").append(toRubyRequirement(spec.requirement)).append("\n") + .append(" xcproj.root_object.package_references << package_ref\n") + .append("end\n"); + for (String product : spec.products) { + script.append("product_dep = target.package_product_dependencies.find { |dep| dep.product_name == '") + .append(escapeRuby(product)).append("' }\n") + .append("if product_dep.nil?\n") + .append(" product_dep = xcproj.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)\n") + .append(" product_dep.package = package_ref\n") + .append(" product_dep.product_name = '").append(escapeRuby(product)).append("'\n") + .append(" target.package_product_dependencies << product_dep\n") + .append("end\n") + .append("unless target.frameworks_build_phase.files_references.any? { |ref| ref.respond_to?(:display_name) && ref.display_name == '") + .append(escapeRuby(product)).append("' }\n") + .append(" build_file = xcproj.new(Xcodeproj::Project::Object::PBXBuildFile)\n") + .append(" build_file.product_ref = product_dep\n") + .append(" target.frameworks_build_phase.files << build_file\n") + .append("end\n"); + } + } + script.append("xcproj.save\n"); + try { + createFile(configFile, script.toString().getBytes(StandardCharsets.UTF_8)); + exec(hooksDir, "chmod", "0755", configFile.getAbsolutePath()); + if (!exec(hooksDir, configFile.getAbsolutePath())) { + throw new BuildException("Failed to configure Swift Package Manager dependencies for generated Xcode project"); + } + } catch (BuildException ex) { + throw ex; + } catch (Exception ex) { + throw new BuildException("Failed to configure Swift Package Manager dependencies for generated Xcode project", ex); + } + } + + private String toRubyRequirement(String requirement) throws BuildException { + if (requirement.startsWith("from:")) { + String min = requirement.substring("from:".length()).trim(); + return "{ 'kind' => 'upToNextMajorVersion', 'minimumVersion' => '" + escapeRuby(min) + "' }"; + } + if (requirement.startsWith("exact:")) { + String version = requirement.substring("exact:".length()).trim(); + return "{ 'kind' => 'exactVersion', 'version' => '" + escapeRuby(version) + "' }"; + } + if (requirement.startsWith("branch:")) { + String branch = requirement.substring("branch:".length()).trim(); + return "{ 'kind' => 'branch', 'branch' => '" + escapeRuby(branch) + "' }"; + } + if (requirement.startsWith("revision:")) { + String revision = requirement.substring("revision:".length()).trim(); + return "{ 'kind' => 'revision', 'revision' => '" + escapeRuby(revision) + "' }"; + } + if (requirement.startsWith("range:")) { + String[] bounds = requirement.substring("range:".length()).trim().split("\\.\\.<"); + if (bounds.length != 2) { + throw new BuildException("Invalid SPM range requirement '" + requirement + "'"); + } + return "{ 'kind' => 'versionRange', 'minimumVersion' => '" + escapeRuby(bounds[0].trim()) + "', 'maximumVersion' => '" + escapeRuby(bounds[1].trim()) + "' }"; + } + throw new BuildException("Unsupported SPM requirement '" + requirement + "'"); + } + + private void ensureTopLevelWorkspace(BuildRequest request) throws BuildException { + File distDir = new File(tmpFile, "dist"); + File workspaceDir = new File(distDir, request.getMainClass() + ".xcworkspace"); + if (workspaceDir.exists()) { + return; + } + if (!workspaceDir.mkdirs() && !workspaceDir.isDirectory()) { + throw new BuildException("Failed to create workspace directory " + workspaceDir.getAbsolutePath()); + } + File workspaceData = new File(workspaceDir, "contents.xcworkspacedata"); + String contents = "\n" + + "\n" + + " \n" + + " \n" + + "\n"; + try { + createFile(workspaceData, contents.getBytes(StandardCharsets.UTF_8)); + } catch (IOException ex) { + throw new BuildException("Failed to create workspace metadata at " + workspaceData.getAbsolutePath(), ex); + } + } + private void appendFilesToXcodeProjGroup(StringBuilder sb, File dir, String serviceGroupVarName, String serviceTargetVarName, File baseDir) { String basePath = baseDir.getAbsolutePath(); diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java index c1914d10bf..98579b9101 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java @@ -919,6 +919,18 @@ private File getWorkspace(Properties props, File xcprojectRoot) { return new File(xcprojectRoot, props.getProperty("codename1.mainName")+".xcworkspace"); } + private File getXcodeProject(Properties props, File xcprojectRoot) { + return new File(xcprojectRoot, props.getProperty("codename1.mainName")+".xcodeproj"); + } + + private File getWorkspaceOrProject(Properties props, File xcprojectRoot) { + File workspace = getWorkspace(props, xcprojectRoot); + if (workspace.exists()) { + return workspace; + } + return getXcodeProject(props, xcprojectRoot); + } + private void openWorkspace(File workspace) throws MojoExecutionException { try { ProcessBuilder pb = new ProcessBuilder("open", workspace.getAbsolutePath()); @@ -944,8 +956,9 @@ private void doIOSLocalBuild(File tmpProjectDir, Properties props, File distJar) if (getSourcesModificationTime() <= lastModifiedRecursive(generatedProject)) { getLog().info("Sources have not changed. Skipping Xcode project generation"); if (open) { - getLog().info("Opening workspace project "+getWorkspace(props, generatedProject)); - openWorkspace(getWorkspace(props, generatedProject)); + File projectToOpen = getWorkspaceOrProject(props, generatedProject); + getLog().info("Opening Xcode project "+projectToOpen); + openWorkspace(projectToOpen); } return; @@ -1035,8 +1048,9 @@ private void doIOSLocalBuild(File tmpProjectDir, Properties props, File distJar) } if (open) { - getLog().info("Opening workspace project "+getWorkspace(props, output)); - openWorkspace(getWorkspace(props, output)); + File projectToOpen = getWorkspaceOrProject(props, output); + getLog().info("Opening Xcode project "+projectToOpen); + openWorkspace(projectToOpen); } } diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/IPhoneBuilderDependencyConfigTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/IPhoneBuilderDependencyConfigTest.java new file mode 100644 index 0000000000..3b4f48bae4 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/IPhoneBuilderDependencyConfigTest.java @@ -0,0 +1,156 @@ +package com.codename1.builders; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IPhoneBuilderDependencyConfigTest { + + @Test + void autoResolvesPodsOnly() throws Exception { + BuildRequest request = requestWithArgs( + "ios.dependencyManager", "auto" + ); + + IOSDependencyConfig config = IOSDependencyManager.resolve(request, "AFNetworking"); + assertEquals(IOSDependencyManager.COCOAPODS, config.mode); + assertTrue(config.usesCocoaPods()); + assertFalse(config.usesSwiftPackages()); + assertEquals("AFNetworking", config.iosPods); + } + + @Test + void autoResolvesSpmOnly() throws Exception { + BuildRequest request = requestWithArgs( + "ios.dependencyManager", "auto", + "ios.spm.packages", "swift-collections|https://github.com/apple/swift-collections.git|from:1.1.0", + "ios.spm.products.swift-collections", "Collections" + ); + + IOSDependencyConfig config = IOSDependencyManager.resolve(request, ""); + assertEquals(IOSDependencyManager.SPM, config.mode); + assertTrue(config.usesSwiftPackages()); + assertFalse(config.usesCocoaPods()); + assertEquals(1, config.swiftPackages.size()); + assertEquals("Collections", config.swiftPackages.get(0).products.get(0)); + } + + @Test + void autoResolvesBothWhenBothHintFamiliesPresent() throws Exception { + BuildRequest request = requestWithArgs( + "ios.spm.packages", "swift-collections|https://github.com/apple/swift-collections.git|from:1.1.0", + "ios.spm.products.swift-collections", "Collections" + ); + + IOSDependencyConfig config = IOSDependencyManager.resolve(request, "AFNetworking"); + assertEquals(IOSDependencyManager.BOTH, config.mode); + } + + @Test + void autoResolvesNoneWithoutDependencyHints() throws Exception { + IOSDependencyConfig config = IOSDependencyManager.resolve(new BuildRequest(), ""); + assertEquals(IOSDependencyManager.NONE, config.mode); + assertTrue(config.swiftPackages.isEmpty()); + } + + @Test + void explicitSpmRequiresSpmPackages() { + BuildRequest request = requestWithArgs("ios.dependencyManager", "spm"); + BuildException ex = assertThrows(BuildException.class, () -> IOSDependencyManager.resolve(request, "")); + assertTrue(ex.getMessage().contains("ios.spm.packages")); + } + + @Test + void explicitCocoaPodsRequiresPods() { + BuildRequest request = requestWithArgs("ios.dependencyManager", "cocoapods"); + BuildException ex = assertThrows(BuildException.class, () -> IOSDependencyManager.resolve(request, "")); + assertTrue(ex.getMessage().contains("ios.pods")); + } + + @Test + void explicitBothRequiresBothHintFamilies() { + BuildRequest request = requestWithArgs( + "ios.dependencyManager", "both", + "ios.spm.packages", "swift-collections|https://github.com/apple/swift-collections.git|from:1.1.0", + "ios.spm.products.swift-collections", "Collections" + ); + BuildException ex = assertThrows(BuildException.class, () -> IOSDependencyManager.resolve(request, "")); + assertTrue(ex.getMessage().contains("both ios.pods and ios.spm.packages")); + } + + @Test + void parsesSupportedSwiftPackageRequirementsAndProducts() throws Exception { + BuildRequest request = requestWithArgs( + "ios.spm.packages", + "pkg1|https://example.com/pkg1.git|from:1.2.3;" + + "pkg2|https://example.com/pkg2.git|exact:2.0.0;" + + "pkg3|https://example.com/pkg3.git|branch:main;" + + "pkg4|https://example.com/pkg4.git|revision:abc123;" + + "pkg5|https://example.com/pkg5.git|range:1.0.0..<2.0.0", + "ios.spm.products.pkg1", "P1,P1Support", + "ios.spm.products.pkg2", "P2", + "ios.spm.products.pkg3", "P3", + "ios.spm.products.pkg4", "P4", + "ios.spm.products.pkg5", "P5" + ); + + List specs = SwiftPackageSpec.parse(request); + assertEquals(5, specs.size()); + assertEquals("pkg1", specs.get(0).identity); + assertEquals("https://example.com/pkg1.git", specs.get(0).url); + assertEquals(2, specs.get(0).products.size()); + assertEquals("range:1.0.0..<2.0.0", specs.get(4).requirement); + } + + @Test + void rejectsMalformedSwiftPackageEntry() { + BuildRequest request = requestWithArgs( + "ios.spm.packages", "swift-collections|https://github.com/apple/swift-collections.git", + "ios.spm.products.swift-collections", "Collections" + ); + assertThrows(BuildException.class, () -> SwiftPackageSpec.parse(request)); + } + + @Test + void rejectsSwiftPackageWithoutProducts() { + BuildRequest request = requestWithArgs( + "ios.spm.packages", "swift-collections|https://github.com/apple/swift-collections.git|from:1.1.0" + ); + BuildException ex = assertThrows(BuildException.class, () -> SwiftPackageSpec.parse(request)); + assertTrue(ex.getMessage().contains("ios.spm.products.swift-collections")); + } + + @Test + void rejectsInvalidRangeRequirement() { + BuildRequest request = requestWithArgs( + "ios.spm.packages", "swift-collections|https://github.com/apple/swift-collections.git|range:1.1.0", + "ios.spm.products.swift-collections", "Collections" + ); + BuildException ex = assertThrows(BuildException.class, () -> SwiftPackageSpec.parse(request)); + assertTrue(ex.getMessage().contains("range")); + } + + @Test + void dependencyManagerHintParsingIsCaseInsensitive() throws Exception { + assertEquals(IOSDependencyManager.SPM, IOSDependencyManager.fromHint("SpM")); + assertEquals(IOSDependencyManager.AUTO, IOSDependencyManager.fromHint("")); + } + + @Test + void rejectsUnknownDependencyManagerHint() { + assertThrows(BuildException.class, () -> IOSDependencyManager.fromHint("gradle")); + } + + private BuildRequest requestWithArgs(String... kvPairs) { + BuildRequest out = new BuildRequest(); + for (int i = 0; i < kvPairs.length; i += 2) { + out.putArgument(kvPairs[i], kvPairs[i + 1]); + } + return out; + } +} diff --git a/scripts/build-ios-app.sh b/scripts/build-ios-app.sh index 6484dd645c..45cae0ca30 100755 --- a/scripts/build-ios-app.sh +++ b/scripts/build-ios-app.sh @@ -54,24 +54,29 @@ if ! command -v xcodebuild >/dev/null 2>&1; then bia_log "xcodebuild not found. Install Xcode command-line tools." >&2 exit 1 fi -if ! command -v pod >/dev/null 2>&1; then - bia_log "CocoaPods (pod) command not found. Install cocoapods before running this script." >&2 - exit 1 -fi - export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH" BASE_PATH="$PATH" bia_log "Using JAVA_HOME at $JAVA_HOME" bia_log "Using JAVA17_HOME at $JAVA17_HOME" bia_log "Using Maven installation at $MAVEN_HOME" -bia_log "Using CocoaPods version $(pod --version 2>/dev/null || echo '')" +if command -v pod >/dev/null 2>&1; then + bia_log "Using CocoaPods version $(pod --version 2>/dev/null || echo '')" +else + bia_log "CocoaPods command not found; pod install will be skipped unless the generated project requires it" +fi bia_log "Java version for baseline toolchain:" "$JAVA_HOME/bin/java" -version bia_log "Using JAVAC from JAVA17_HOME for demo compilation:" "$JAVA17_HOME/bin/javac" -version IOS_UISCENE="${IOS_UISCENE:-false}" bia_log "Building sample app with ios.uiscene=${IOS_UISCENE}" +EXTRA_IOS_ARGS=() +if [ -n "${IOS_DEPENDENCY_ARGS:-}" ]; then + # shellcheck disable=SC2206 + EXTRA_IOS_ARGS=(${IOS_DEPENDENCY_ARGS}) + bia_log "Applying extra iOS build args: ${IOS_DEPENDENCY_ARGS}" +fi APP_DIR="scripts/hellocodenameone" @@ -100,6 +105,7 @@ bia_log "Running HelloCodenameOne Maven build with JAVA_HOME=$JAVA17_HOME" -Dmaven.compiler.executable="$JAVA17_HOME/bin/javac" \ -Dcodename1.arg.ios.uiscene="${IOS_UISCENE}" \ -Dopen=false \ + "${EXTRA_IOS_ARGS[@]}" \ -U -e -X > "$MVN_IOS_LOG" 2>&1 RC=$? set -e @@ -151,8 +157,11 @@ if [ -z "$PROJECT_DIR" ]; then fi bia_log "Found generated iOS project at $PROJECT_DIR" -# CocoaPods (project contains a Podfile but usually empty — fine) if [ -f "$PROJECT_DIR/Podfile" ]; then + if ! command -v pod >/dev/null 2>&1; then + bia_log "Generated project requires CocoaPods but the pod command is not installed." >&2 + exit 1 + fi bia_log "Installing CocoaPods dependencies" POD_START=$(date +%s) ( @@ -169,7 +178,7 @@ else bia_log "Podfile not found in generated project; skipping pod install" fi -# Locate workspace for the next step +# Locate workspace or project for the next step WORKSPACE="" for candidate in "$PROJECT_DIR"/*.xcworkspace; do if [ -d "$candidate" ]; then @@ -178,11 +187,19 @@ for candidate in "$PROJECT_DIR"/*.xcworkspace; do fi done if [ -z "$WORKSPACE" ]; then - bia_log "Failed to locate xcworkspace in $PROJECT_DIR" >&2 + for candidate in "$PROJECT_DIR"/*.xcodeproj; do + if [ -d "$candidate" ]; then + WORKSPACE="$candidate" + break + fi + done +fi +if [ -z "$WORKSPACE" ]; then + bia_log "Failed to locate xcworkspace or xcodeproj in $PROJECT_DIR" >&2 ls "$PROJECT_DIR" >&2 || true exit 1 fi -bia_log "Found xcworkspace: $WORKSPACE" +bia_log "Found Xcode entrypoint: $WORKSPACE" # Make these visible to the next GH Actions step @@ -198,6 +215,10 @@ bia_log "Emitted outputs -> workspace=$WORKSPACE, scheme=HelloCodenameOne" # (Optional) dump xcodebuild -list for debugging ARTIFACTS_DIR="${ARTIFACTS_DIR:-$REPO_ROOT/artifacts}" mkdir -p "$ARTIFACTS_DIR" -xcodebuild -workspace "$WORKSPACE" -list > "$ARTIFACTS_DIR/xcodebuild-list.txt" 2>&1 || true +if [[ "$WORKSPACE" == *.xcworkspace ]]; then + xcodebuild -workspace "$WORKSPACE" -list > "$ARTIFACTS_DIR/xcodebuild-list.txt" 2>&1 || true +else + xcodebuild -project "$WORKSPACE" -list > "$ARTIFACTS_DIR/xcodebuild-list.txt" 2>&1 || true +fi exit 0 diff --git a/scripts/run-ios-native-tests.sh b/scripts/run-ios-native-tests.sh index e2ae3f6c7b..f879a4768d 100755 --- a/scripts/run-ios-native-tests.sh +++ b/scripts/run-ios-native-tests.sh @@ -19,16 +19,25 @@ APP_SCHEME="${2:-}" TEST_SCHEME="${3:-}" if [ ! -d "$WORKSPACE_PATH" ]; then - ri_log "Workspace not found at $WORKSPACE_PATH" >&2 + ri_log "Xcode workspace/project not found at $WORKSPACE_PATH" >&2 exit 3 fi +XCODE_CONTAINER_FLAG="-workspace" +if [[ "$WORKSPACE_PATH" == *.xcodeproj ]]; then + XCODE_CONTAINER_FLAG="-project" +fi + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$REPO_ROOT" if [ -z "$APP_SCHEME" ]; then - APP_SCHEME="$(basename "$WORKSPACE_PATH" .xcworkspace)" + if [[ "$WORKSPACE_PATH" == *.xcworkspace ]]; then + APP_SCHEME="$(basename "$WORKSPACE_PATH" .xcworkspace)" + else + APP_SCHEME="$(basename "$WORKSPACE_PATH" .xcodeproj)" + fi fi if [ -z "$TEST_SCHEME" ]; then TEST_SCHEME="${APP_SCHEME}Tests" @@ -40,7 +49,7 @@ ri_log "Injecting native notification tests into project at $PROJECT_DIR" "$REPO_ROOT/scripts/ios/notification-tests/install-native-notification-tests.sh" "$PROJECT_DIR" ri_log "Discovering simulator destination for test scheme $TEST_SCHEME" -DESTINATION="$(xcodebuild -workspace "$WORKSPACE_PATH" -scheme "$TEST_SCHEME" -showdestinations 2>/dev/null \ +DESTINATION="$(xcodebuild "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$TEST_SCHEME" -showdestinations 2>/dev/null \ | sed -n 's/.*{ platform:iOS Simulator,.*id:\([^,}]*\).*/\1/p' \ | rg -v "placeholder" \ | head -n 1 \ @@ -50,7 +59,7 @@ if [ -z "$DESTINATION" ]; then fi SIMULATOR_ID="$(printf "%s" "$DESTINATION" | sed -n 's/.*id=\([^,]*\).*/\1/p')" -BUNDLE_ID="$(xcodebuild -workspace "$WORKSPACE_PATH" -scheme "$APP_SCHEME" -showBuildSettings 2>/dev/null \ +BUNDLE_ID="$(xcodebuild "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$APP_SCHEME" -showBuildSettings 2>/dev/null \ | sed -n 's/^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = //p' \ | head -n 1 || true)" @@ -71,7 +80,7 @@ TEST_LOG="$ARTIFACTS_DIR/xcode-native-tests.log" ri_log "Running xcodebuild test (scheme=$TEST_SCHEME, destination=$DESTINATION)" set +e xcodebuild \ - -workspace "$WORKSPACE_PATH" \ + "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" \ -scheme "$TEST_SCHEME" \ -destination "$DESTINATION" \ test | tee "$TEST_LOG" diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh index c682cf2ca5..a7f5ebcea1 100755 --- a/scripts/run-ios-ui-tests.sh +++ b/scripts/run-ios-ui-tests.sh @@ -22,10 +22,15 @@ if [ -n "$APP_BUNDLE_PATH" ] && [ ! -d "$APP_BUNDLE_PATH" ] && [ -z "$REQUESTED_ fi if [ ! -d "$WORKSPACE_PATH" ]; then - ri_log "Workspace not found at $WORKSPACE_PATH" >&2 + ri_log "Xcode workspace/project not found at $WORKSPACE_PATH" >&2 exit 3 fi +XCODE_CONTAINER_FLAG="-workspace" +if [[ "$WORKSPACE_PATH" == *.xcodeproj ]]; then + XCODE_CONTAINER_FLAG="-project" +fi + if [ -n "$APP_BUNDLE_PATH" ]; then ri_log "Using simulator app bundle at $APP_BUNDLE_PATH" fi @@ -123,6 +128,8 @@ fi if [ -z "$REQUESTED_SCHEME" ]; then if [[ "$WORKSPACE_PATH" == *.xcworkspace ]]; then REQUESTED_SCHEME="$(basename "$WORKSPACE_PATH" .xcworkspace)" + elif [[ "$WORKSPACE_PATH" == *.xcodeproj ]]; then + REQUESTED_SCHEME="$(basename "$WORKSPACE_PATH" .xcodeproj)" else REQUESTED_SCHEME="$(basename "$WORKSPACE_PATH")" fi @@ -141,6 +148,13 @@ export CN1SS_PREVIEW_DIR="$SCREENSHOT_PREVIEW_DIR" # Patch scheme env vars to point to our runtime dirs SCHEME_FILE="$WORKSPACE_PATH/xcshareddata/xcschemes/$SCHEME.xcscheme" +if [ ! -f "$SCHEME_FILE" ] && [[ "$WORKSPACE_PATH" == *.xcworkspace ]]; then + PROJECT_DIR="$(cd "$(dirname "$WORKSPACE_PATH")" && pwd)" + PROJECT_SCHEME_FILE="$PROJECT_DIR/$(basename "$WORKSPACE_PATH" .xcworkspace).xcodeproj/xcshareddata/xcschemes/$SCHEME.xcscheme" + if [ -f "$PROJECT_SCHEME_FILE" ]; then + SCHEME_FILE="$PROJECT_SCHEME_FILE" + fi +fi if [ -f "$SCHEME_FILE" ]; then if sed --version >/dev/null 2>&1; then # GNU sed @@ -210,7 +224,7 @@ normalize_destination() { auto_select_destination() { local show_dest rc=0 best_line="" best_key="" line payload platform id name os priority key part value set +e - show_dest="$(xcodebuild -workspace "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -showdestinations 2>/dev/null)" + show_dest="$(xcodebuild "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -showdestinations 2>/dev/null)" rc=$? set -e @@ -364,13 +378,13 @@ if [ -z "$SIM_DESTINATION" ]; then else ri_log "Simulator auto-selection did not return a destination" SHOW_DEST_LOG="$ARTIFACTS_DIR/xcodebuild-showdestinations.log" - xcodebuild -workspace "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -showdestinations \ + xcodebuild "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -showdestinations \ > "$SHOW_DEST_LOG" 2>&1 || true if grep -q "not installed" "$SHOW_DEST_LOG"; then if [ "$DOWNLOAD_PLATFORMS" = "true" ]; then ri_log "Attempting to download missing iOS platform via xcodebuild -downloadPlatform iOS" xcodebuild -downloadPlatform iOS || true - xcodebuild -workspace "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -showdestinations \ + xcodebuild "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -showdestinations \ > "$SHOW_DEST_LOG" 2>&1 || true else ri_log "Destinations report missing platforms. Set XCODE_DOWNLOAD_PLATFORMS=true to attempt auto-download." @@ -415,7 +429,7 @@ BUILD_LOG="$ARTIFACTS_DIR/xcodebuild-build.log" ri_log "Building simulator app with xcodebuild" COMPILE_START=$(date +%s) if ! xcodebuild \ - -workspace "$WORKSPACE_PATH" \ + "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" \ -scheme "$SCHEME" \ -sdk iphonesimulator \ -configuration Debug \ @@ -430,7 +444,7 @@ COMPILE_END=$(date +%s) COMPILATION_TIME=$((COMPILE_END - COMPILE_START)) ri_log "Compilation time: ${COMPILATION_TIME}s" -BUILD_SETTINGS="$(xcodebuild -workspace "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -configuration Debug -showBuildSettings 2>/dev/null || true)" +BUILD_SETTINGS="$(xcodebuild "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$SCHEME" -sdk iphonesimulator -configuration Debug -showBuildSettings 2>/dev/null || true)" TARGET_BUILD_DIR="$(printf '%s\n' "$BUILD_SETTINGS" | awk -F' = ' '/ TARGET_BUILD_DIR /{print $2; exit}')" WRAPPER_NAME="$(printf '%s\n' "$BUILD_SETTINGS" | awk -F' = ' '/ WRAPPER_NAME /{print $2; exit}')" if [ -z "$WRAPPER_NAME" ]; then From 88467cf797f1f428aa5731c2f4207983c2696715 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:18:15 +0200 Subject: [PATCH 2/3] Fixed CI issues --- .github/workflows/ios-packaging.yml | 2 +- .github/workflows/scripts-ios.yml | 2 +- .../com/codename1/builders/IPhoneBuilder.java | 6 ++- scripts/build-ios-app.sh | 43 +++++++++++++---- scripts/run-ios-native-tests.sh | 4 +- scripts/run-ios-ui-tests.sh | 47 +++++++++++++++---- .../template.xcodeproj/project.pbxproj | 16 +++---- 7 files changed, 88 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ios-packaging.yml b/.github/workflows/ios-packaging.yml index 0c7ef98b1e..8881ee75f8 100644 --- a/.github/workflows/ios-packaging.yml +++ b/.github/workflows/ios-packaging.yml @@ -91,7 +91,7 @@ jobs: - name: Setup workspace run: ./scripts/setup-workspace.sh -q -DskipTests - timeout-minutes: 25 + timeout-minutes: 40 - name: Build iOS port run: ./scripts/build-ios-port.sh -q -DskipTests diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index 4d44b98c36..18abe240e2 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -127,7 +127,7 @@ jobs: - name: Setup workspace run: ./scripts/setup-workspace.sh -q -DskipTests # per-step timeout - timeout-minutes: 25 + timeout-minutes: 40 - name: Build iOS port run: ./scripts/build-ios-port.sh -q -DskipTests diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index e800f8c16d..e635fb255d 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -1634,9 +1634,11 @@ public void usesClassMethod(String cls, String method) { // that the change will work for all builds. I made it "only" for the cocoapods version // to prevent inadvertent breaking of versioned builds etc... if (runPods) { - replaceAllInFile(pbx, "ARCHS = [^;]+;", "ARCHS = \"\\$(ARCHS_STANDARD_INCLUDING_64_BIT)\";"); + replaceAllInFile(pbx, "ARCHS = [^;]+;", "ARCHS = \"\\$(ARCHS_STANDARD)\";"); + replaceAllInFile(pbx, "VALID_ARCHS = [^;]+;", "VALID_ARCHS = \"\\$(ARCHS_STANDARD)\";"); } else { - replaceInFile(pbx, "ARCHS = armv7;", "ARCHS = \"armv7 arm64\";"); + replaceInFile(pbx, "ARCHS = armv7;", "ARCHS = \"\\$(ARCHS_STANDARD)\";"); + replaceAllInFile(pbx, "VALID_ARCHS = [^;]+;", "VALID_ARCHS = \"\\$(ARCHS_STANDARD)\";"); } } diff --git a/scripts/build-ios-app.sh b/scripts/build-ios-app.sh index 45cae0ca30..c88bc3e795 100755 --- a/scripts/build-ios-app.sh +++ b/scripts/build-ios-app.sh @@ -96,17 +96,22 @@ bia_log "Running HelloCodenameOne Maven build with JAVA_HOME=$JAVA17_HOME" export JAVA_HOME="$JAVA17_HOME" export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$BASE_PATH" MVN_IOS_LOG="$ARTIFACTS_DIR/hellocn1-ios-build.log" + MVN_CMD=( + ./mvnw package + -DskipTests + -Dcodename1.platform=ios + -Dcodename1.buildTarget=ios-source + -Dmaven.compiler.fork=true + -Dmaven.compiler.executable="$JAVA17_HOME/bin/javac" + -Dcodename1.arg.ios.uiscene="${IOS_UISCENE}" + -Dopen=false + ) + if [ ${#EXTRA_IOS_ARGS[@]} -gt 0 ]; then + MVN_CMD+=("${EXTRA_IOS_ARGS[@]}") + fi + MVN_CMD+=(-U -e -X) set +e - ./mvnw package \ - -DskipTests \ - -Dcodename1.platform=ios \ - -Dcodename1.buildTarget=ios-source \ - -Dmaven.compiler.fork=true \ - -Dmaven.compiler.executable="$JAVA17_HOME/bin/javac" \ - -Dcodename1.arg.ios.uiscene="${IOS_UISCENE}" \ - -Dopen=false \ - "${EXTRA_IOS_ARGS[@]}" \ - -U -e -X > "$MVN_IOS_LOG" 2>&1 + "${MVN_CMD[@]}" > "$MVN_IOS_LOG" 2>&1 RC=$? set -e if [ $RC -ne 0 ]; then @@ -178,6 +183,24 @@ else bia_log "Podfile not found in generated project; skipping pod install" fi +WORKSPACE_XML=' + + + +' +if [ ! -d "$PROJECT_DIR/HelloCodenameOne.xcworkspace" ] && [ -d "$PROJECT_DIR/HelloCodenameOne.xcodeproj" ]; then + bia_log "Creating fallback xcworkspace for generated Xcode project" + mkdir -p "$PROJECT_DIR/HelloCodenameOne.xcworkspace" + printf '%s\n' "$WORKSPACE_XML" > "$PROJECT_DIR/HelloCodenameOne.xcworkspace/contents.xcworkspacedata" +fi + +if [ -d "$PROJECT_DIR/HelloCodenameOne.xcodeproj" ]; then + bia_log "Ensuring shared Xcode scheme exists" + "$REPO_ROOT/scripts/ios/create-shared-scheme.py" "$PROJECT_DIR" HelloCodenameOne +fi + # Locate workspace or project for the next step WORKSPACE="" for candidate in "$PROJECT_DIR"/*.xcworkspace; do diff --git a/scripts/run-ios-native-tests.sh b/scripts/run-ios-native-tests.sh index f879a4768d..058197ca83 100755 --- a/scripts/run-ios-native-tests.sh +++ b/scripts/run-ios-native-tests.sh @@ -47,11 +47,13 @@ PROJECT_DIR="$(cd "$(dirname "$WORKSPACE_PATH")" && pwd)" ri_log "Injecting native notification tests into project at $PROJECT_DIR" "$REPO_ROOT/scripts/ios/notification-tests/install-native-notification-tests.sh" "$PROJECT_DIR" +"$REPO_ROOT/scripts/ios/create-shared-scheme.py" "$PROJECT_DIR" "$APP_SCHEME" +"$REPO_ROOT/scripts/ios/create-shared-scheme.py" "$PROJECT_DIR" "$TEST_SCHEME" ri_log "Discovering simulator destination for test scheme $TEST_SCHEME" DESTINATION="$(xcodebuild "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" -scheme "$TEST_SCHEME" -showdestinations 2>/dev/null \ | sed -n 's/.*{ platform:iOS Simulator,.*id:\([^,}]*\).*/\1/p' \ - | rg -v "placeholder" \ + | grep -v "placeholder" \ | head -n 1 \ | sed 's#^#platform=iOS Simulator,id=#' || true)" if [ -z "$DESTINATION" ]; then diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh index a7f5ebcea1..a6328ceb2e 100755 --- a/scripts/run-ios-ui-tests.sh +++ b/scripts/run-ios-ui-tests.sh @@ -370,6 +370,7 @@ fallback_sim_destination() { SIM_DESTINATION="${IOS_SIM_DESTINATION:-}" +USE_GENERIC_BUILD_DESTINATION="false" if [ -z "$SIM_DESTINATION" ]; then SELECTED_DESTINATION="$(auto_select_destination || true)" if [ -n "${SELECTED_DESTINATION:-}" ]; then @@ -400,14 +401,20 @@ if [ -z "$SIM_DESTINATION" ]; then FALLBACK_DESTINATION="$(fallback_sim_destination || true)" if [ -n "${FALLBACK_DESTINATION:-}" ]; then SIM_DESTINATION="$FALLBACK_DESTINATION" + USE_GENERIC_BUILD_DESTINATION="true" ri_log "Using fallback simulator destination '$SIM_DESTINATION'" else SIM_DESTINATION="platform=iOS Simulator,name=iPhone 16" + USE_GENERIC_BUILD_DESTINATION="true" ri_log "Falling back to default simulator destination '$SIM_DESTINATION'" fi fi SIM_DESTINATION="$(normalize_destination "$SIM_DESTINATION")" +BUILD_DESTINATION="$SIM_DESTINATION" +if [ "$USE_GENERIC_BUILD_DESTINATION" = "true" ]; then + BUILD_DESTINATION="generic/platform=iOS Simulator" +fi # Extract UDID and prefer id-only destination to avoid OS/SDK mismatches SIM_UDID="$(printf '%s\n' "$SIM_DESTINATION" | sed -n 's/.*id=\([^,]*\).*/\1/p' | tr -d '\r[:space:]')" @@ -420,23 +427,45 @@ if [ -n "$SIM_UDID" ]; then echo "Simulator Boot : $(( (BOOT_END - BOOT_START) * 1000 )) ms" >> "$ARTIFACTS_DIR/ios-test-stats.txt" SIM_DESTINATION="id=$SIM_UDID" fi +if [ "$USE_GENERIC_BUILD_DESTINATION" = "true" ]; then + ri_log "Building with generic simulator destination '$BUILD_DESTINATION' and running on '$SIM_DESTINATION'" +else + BUILD_DESTINATION="$SIM_DESTINATION" +fi ri_log "Running DeviceRunner on destination '$SIM_DESTINATION'" +HOST_ARCH="$(uname -m 2>/dev/null || echo arm64)" +case "$HOST_ARCH" in + arm64|x86_64) BUILD_ARCH="$HOST_ARCH" ;; + *) BUILD_ARCH="arm64" ;; +esac + DERIVED_DATA_DIR="$SCREENSHOT_TMP_DIR/derived" rm -rf "$DERIVED_DATA_DIR" BUILD_LOG="$ARTIFACTS_DIR/xcodebuild-build.log" ri_log "Building simulator app with xcodebuild" COMPILE_START=$(date +%s) -if ! xcodebuild \ - "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" \ - -scheme "$SCHEME" \ - -sdk iphonesimulator \ - -configuration Debug \ - -destination "$SIM_DESTINATION" \ - -destination-timeout 120 \ - -derivedDataPath "$DERIVED_DATA_DIR" \ - build | tee "$BUILD_LOG"; then +XCODE_BUILD_CMD=( + xcodebuild + "$XCODE_CONTAINER_FLAG" "$WORKSPACE_PATH" + -scheme "$SCHEME" + -sdk iphonesimulator + -configuration Debug + -destination "$BUILD_DESTINATION" + -destination-timeout 120 + -derivedDataPath "$DERIVED_DATA_DIR" +) +if [ "$USE_GENERIC_BUILD_DESTINATION" = "true" ]; then + ri_log "Forcing simulator ARCHS=$BUILD_ARCH for generic build destination" + XCODE_BUILD_CMD+=( + "ARCHS=$BUILD_ARCH" + "ONLY_ACTIVE_ARCH=YES" + "EXCLUDED_ARCHS=armv7 armv7s" + ) +fi +XCODE_BUILD_CMD+=(build) +if ! "${XCODE_BUILD_CMD[@]}" | tee "$BUILD_LOG"; then ri_log "STAGE:XCODE_BUILD_FAILED -> See $BUILD_LOG" exit 10 fi diff --git a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj index 5de7bc3890..c6f0852db6 100644 --- a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj +++ b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj @@ -270,7 +270,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = armv7; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = NO; @@ -304,7 +304,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; - VALID_ARCHS = "armv7 arm64"; + VALID_ARCHS = "$(ARCHS_STANDARD)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/template-src", @@ -316,7 +316,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = armv7; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = NO; @@ -345,7 +345,7 @@ SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; - VALID_ARCHS = "armv7 arm64"; + VALID_ARCHS = "$(ARCHS_STANDARD)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/template-src", @@ -362,7 +362,7 @@ GCC_PREFIX_HEADER = "template-src/template-Prefix.pch"; INFOPLIST_FILE = "template-src/template-Info.plist"; PRODUCT_NAME = "$(TARGET_NAME)"; - VALID_ARCHS = "armv7 arm64"; + VALID_ARCHS = "$(ARCHS_STANDARD)"; WRAPPER_EXTENSION = app; }; name = Debug; @@ -377,7 +377,7 @@ GCC_PREFIX_HEADER = "template-src/template-Prefix.pch"; INFOPLIST_FILE = "template-src/template-Info.plist"; PRODUCT_NAME = "$(TARGET_NAME)"; - VALID_ARCHS = "armv7 arm64"; + VALID_ARCHS = "$(ARCHS_STANDARD)"; WRAPPER_EXTENSION = app; }; name = Release; @@ -385,7 +385,7 @@ 0F634EB618E9ABBC002F3D1D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; + ARCHS = "$(ARCHS_STANDARD)"; BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/template.app/template-src"; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", @@ -408,7 +408,7 @@ 0F634EB718E9ABBC002F3D1D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = armv7; + ARCHS = "$(ARCHS_STANDARD)"; BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/template.app/template-src"; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", From bd0741fd3dc5c7727272aab91741f9082fd0e615 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:56:48 +0200 Subject: [PATCH 3/3] Split ci script to solve timeout issues --- .github/workflows/ios-packaging.yml | 2 +- .github/workflows/scripts-ios-native.yml | 140 +++++++++++++++++++++++ .github/workflows/scripts-ios.yml | 15 +-- 3 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/scripts-ios-native.yml diff --git a/.github/workflows/ios-packaging.yml b/.github/workflows/ios-packaging.yml index 8881ee75f8..afe8051c82 100644 --- a/.github/workflows/ios-packaging.yml +++ b/.github/workflows/ios-packaging.yml @@ -95,7 +95,7 @@ jobs: - name: Build iOS port run: ./scripts/build-ios-port.sh -q -DskipTests - timeout-minutes: 25 + timeout-minutes: 40 - name: Build sample iOS app id: build_ios_app diff --git a/.github/workflows/scripts-ios-native.yml b/.github/workflows/scripts-ios-native.yml new file mode 100644 index 0000000000..042b4fb0e6 --- /dev/null +++ b/.github/workflows/scripts-ios-native.yml @@ -0,0 +1,140 @@ +name: Test iOS native test scripts + +on: + pull_request: + paths: + - '.github/workflows/scripts-ios-native.yml' + - 'scripts/setup-workspace.sh' + - 'scripts/build-ios-port.sh' + - 'scripts/build-ios-app.sh' + - 'scripts/run-ios-native-tests.sh' + - 'scripts/ios/create-shared-scheme.py' + - 'scripts/ios/notification-tests/native-tests/**' + - 'scripts/ios/notification-tests/install-native-notification-tests.sh' + - 'scripts/ios/notification-tests/**' + - 'scripts/hellocodenameone/**' + - 'scripts/templates/**' + - '!scripts/templates/**/*.md' + - 'CodenameOne/src/**' + - '!CodenameOne/src/**/*.md' + - 'Ports/iOSPort/**' + - '!Ports/iOSPort/**/*.md' + - 'vm/**' + - '!vm/**/*.md' + - 'tests/**' + - '!tests/**/*.md' + - '!docs/**' + - 'maven/**' + - '!maven/core-unittests/**' + push: + branches: [ master ] + paths: + - '.github/workflows/scripts-ios-native.yml' + - 'scripts/setup-workspace.sh' + - 'scripts/build-ios-port.sh' + - 'scripts/build-ios-app.sh' + - 'scripts/run-ios-native-tests.sh' + - 'scripts/ios/create-shared-scheme.py' + - 'scripts/ios/notification-tests/native-tests/**' + - 'scripts/ios/notification-tests/install-native-notification-tests.sh' + - 'scripts/ios/notification-tests/**' + - 'scripts/hellocodenameone/**' + - 'scripts/templates/**' + - '!scripts/templates/**/*.md' + - 'CodenameOne/src/**' + - '!CodenameOne/src/**/*.md' + - 'Ports/iOSPort/**' + - '!Ports/iOSPort/**/*.md' + - 'vm/**' + - '!vm/**/*.md' + - 'tests/**' + - '!tests/**/*.md' + - '!docs/**' + - 'maven/**' + - '!maven/core-unittests/**' + +jobs: + native-ios: + permissions: + contents: read + runs-on: macos-15 + timeout-minutes: 65 + concurrency: + group: mac-ci-${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v4 + + - name: Ensure CocoaPods tooling + run: | + mkdir -p ~/.codenameone + cp maven/UpdateCodenameOne.jar ~/.codenameone/ + set -euo pipefail + if ! command -v ruby >/dev/null; then + echo "ruby not found"; exit 1 + fi + GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')" + export PATH="$GEM_USER_DIR/bin:$PATH" + gem install cocoapods xcodeproj --no-document --user-install + pod --version + + - name: Compute setup-workspace hash + id: setup_hash + run: | + set -euo pipefail + echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT" + + - name: Set TMPDIR + run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV + + - name: Cache codenameone-tools + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/codenameone-tools + key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + ${{ runner.os }}-cn1-tools- + + - name: Restore cn1-binaries cache + uses: actions/cache@v4 + with: + path: ../cn1-binaries + key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + cn1-binaries-${{ runner.os }}- + + - name: Setup workspace + run: ./scripts/setup-workspace.sh -q -DskipTests + timeout-minutes: 40 + + - name: Build iOS port + run: ./scripts/build-ios-port.sh -q -DskipTests + timeout-minutes: 40 + + - name: Build sample iOS app + id: build_ios_app + env: + IOS_UISCENE: "false" + run: ./scripts/build-ios-app.sh -q -DskipTests + timeout-minutes: 30 + + - name: Run native iOS notification tests (XCTest) + env: + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/native-ios-tests + run: | + set -euo pipefail + mkdir -p "${ARTIFACTS_DIR}" + ./scripts/run-ios-native-tests.sh \ + "${{ steps.build_ios_app.outputs.workspace }}" \ + "${{ steps.build_ios_app.outputs.scheme }}" + timeout-minutes: 25 + + - name: Upload native iOS artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-native-tests + path: artifacts + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index 18abe240e2..de197132a8 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -1,4 +1,4 @@ -name: Test iOS build scripts +name: Test iOS UI build scripts on: pull_request: @@ -131,7 +131,7 @@ jobs: - name: Build iOS port run: ./scripts/build-ios-port.sh -q -DskipTests - timeout-minutes: 25 + timeout-minutes: 40 - name: Build sample iOS app and compile workspace (UIScene on) id: build-ios-app-scene @@ -179,17 +179,6 @@ jobs: "${{ steps.build-ios-app.outputs.scheme }}" timeout-minutes: 30 - - name: Run native iOS notification tests (XCTest) - env: - ARTIFACTS_DIR: ${{ github.workspace }}/artifacts - run: | - set -euo pipefail - mkdir -p "${ARTIFACTS_DIR}" - ./scripts/run-ios-native-tests.sh \ - "${{ steps.build-ios-app.outputs.workspace }}" \ - "${{ steps.build-ios-app.outputs.scheme }}" - timeout-minutes: 20 - - name: Upload iOS artifacts if: always() uses: actions/upload-artifact@v4