Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/apple-dev-skills/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,7 @@ In Progress
- Updated the XcodeGen bootstrap direction to prefer Xcode 16 synchronized folders for ordinary app and test source roots, keep broad recursive paths with `includes` and `excludes` as the fallback, and install checked-in external app entitlement files wired through `.xcconfig`.
- Added default tracked homes for app marketing/build versions, Swift 6 and concurrency settings, user-script sandboxing, macOS app sandbox state, and hardened-runtime state so Xcode GUI build-setting changes can be promoted back into `.xcconfig` files cleanly.
- Added a default asset catalog with app-icon and accent-color placeholders, explicit app-icon config wiring, Swift asset-symbol generation, and dead-code stripping defaults to the generated XcodeGen scaffold.
- Completed the XcodeGen String Catalog bootstrap contract: new SwiftUI apps now include `Localizable.xcstrings`, enable generated localization symbols and Swift string extraction, generate unit-test Info.plists, and exclude placeholder files from synchronized app resources; an opt-in macOS integration test verifies the generated build settings and symbol sources.
- Added `migrate-xcode-project-to-xcodegen` as the explicit owner for non-destructive Xcode-managed to XcodeGen conversion audits and stale XcodeGen modernization planning.
- Tightened Xcode project guidance so tracked `.pbxproj` diffs produced by Xcode, XcodeGen, or other project-aware workflows are treated as critical project state that must be reviewed, staged, and committed before push, merge, release, or cleanup.
- Updated standalone install guidance so `apple-dev-skills` defaults to Codex's Git-backed marketplace add/upgrade flow without an explicit ref, documents the optional `socket` marketplace path for Gale's broader plugin set, and keeps manual local clone marketplaces as development and fallback paths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Describe the supported generator choices for `bootstrap-xcode-app-project`.
- Start app scaffolds with `MARKETING_VERSION = 0.0.1` and `CURRENT_PROJECT_VERSION = 1` in the app `.xcconfig`, and wire generated `Info.plist` version keys to those build settings.
- Keep shared Swift language and concurrency defaults in `Shared.xcconfig`, including `SWIFT_VERSION = 6.0`, so every generated target inherits the same language baseline.
- Create a default `Assets.xcassets` resource catalog with `AppIcon` and `AccentColor` placeholders, enable Swift asset symbol generation, and keep the app icon build setting in the app `.xcconfig`.
- Create `Sources/Resources/Localizable.xcstrings` as the default String Catalog. In `Shared.xcconfig`, enable `LOCALIZATION_PREFERS_STRING_CATALOGS`, `STRING_CATALOG_GENERATE_SYMBOLS`, and `SWIFT_EMIT_LOC_STRINGS` so generated apps export localizable Swift strings and compile generated catalog symbols.
- Set `GENERATE_INFOPLIST_FILE = YES` for the unit-test bundle unless the template supplies an explicit test `Info.plist`. Exclude placeholder `.gitkeep` files from synchronized app sources so they do not become duplicate copied resources.
- Keep common linker and build-behavior defaults such as dead-code stripping in checked-in `.xcconfig` files so Xcode GUI changes have a tracked owner.
- Prefer checked-in external `.entitlements` files for app and extension targets. Wire them through `CODE_SIGN_ENTITLEMENTS` in the target `.xcconfig`; do not generate entitlement contents from inline XcodeGen YAML when the file is expected to be edited through Xcode capabilities.
- Treat Xcode Build Settings UI changes as project overrides until proven otherwise. If a setting belongs in a tracked `.xcconfig`, inspect the generated `.pbxproj` diff after GUI edits and move the intentional value back into the owning config file before regenerating.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ SWIFT_VERSION = 6.0
SWIFT_APPROACHABLE_CONCURRENCY = YES
SWIFT_STRICT_CONCURRENCY = complete
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES
LOCALIZATION_PREFERS_STRING_CATALOGS = YES
STRING_CATALOG_GENERATE_SYMBOLS = YES
SWIFT_EMIT_LOC_STRINGS = YES
Comment thread
gaelic-ghost marked this conversation as resolved.
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES
ENABLE_USER_SCRIPT_SANDBOXING = YES
DEAD_CODE_STRIPPING = YES
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

// Unit-test bundle settings that should not vary by build configuration.
PRODUCT_BUNDLE_IDENTIFIER = __BUNDLE_IDENTIFIER__.tests
GENERATE_INFOPLIST_FILE = YES
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ targets:
sources:
- path: Sources
type: syncedFolder
excludes:
- "**/.gitkeep"
- path: Shared
type: syncedFolder
excludes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import os
import subprocess
import sys
import tempfile
import unittest
from contextlib import contextmanager
Expand Down Expand Up @@ -218,6 +219,7 @@ def test_xcodegen_path_can_succeed_with_fake_tools(self) -> None:
self.assertIn("- path: Sources", project_yml)
self.assertIn("- path: Shared", project_yml)
self.assertIn("- path: Tests", project_yml)
self.assertEqual(project_yml.count('- "**/.gitkeep"'), 2)
self.assertNotIn("- path: Sources/App", project_yml)
self.assertNotIn("- path: Sources/Resources", project_yml)
self.assertNotIn("- path: Sources/Support", project_yml)
Expand Down Expand Up @@ -317,6 +319,18 @@ def test_xcodegen_path_can_succeed_with_fake_tools(self) -> None:
"ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES",
(target / "Configurations" / "Shared.xcconfig").read_text(encoding="utf-8"),
)
self.assertIn(
"LOCALIZATION_PREFERS_STRING_CATALOGS = YES",
(target / "Configurations" / "Shared.xcconfig").read_text(encoding="utf-8"),
)
self.assertIn(
"STRING_CATALOG_GENERATE_SYMBOLS = YES",
(target / "Configurations" / "Shared.xcconfig").read_text(encoding="utf-8"),
)
self.assertIn(
"SWIFT_EMIT_LOC_STRINGS = YES",
(target / "Configurations" / "Shared.xcconfig").read_text(encoding="utf-8"),
)
self.assertIn(
"DEAD_CODE_STRIPPING = YES",
(target / "Configurations" / "Shared.xcconfig").read_text(encoding="utf-8"),
Expand All @@ -329,6 +343,10 @@ def test_xcodegen_path_can_succeed_with_fake_tools(self) -> None:
"PRODUCT_BUNDLE_IDENTIFIER = com.example.DemoApp.tests",
(target / "Configurations" / "Tests.xcconfig").read_text(encoding="utf-8"),
)
self.assertIn(
"GENERATE_INFOPLIST_FILE = YES",
(target / "Configurations" / "Tests.xcconfig").read_text(encoding="utf-8"),
)
self.assertIn("xcodegen_template_paths", payload)
self.assertIn("standard_directory_paths", payload)
self.assertTrue((target / "AGENTS.md").exists())
Expand Down Expand Up @@ -372,6 +390,86 @@ def test_xcodegen_path_can_succeed_with_fake_tools(self) -> None:
self.assertTrue((target / ".github" / "workflows" / "validate-repo-maintenance.yml").exists())
self.assertEqual(payload["validation_result"], "passed (xcodebuild -list)")

@unittest.skipUnless(
sys.platform == "darwin" and os.environ.get("APPLE_DEV_SKILLS_RUN_XCODEGEN_INTEGRATION") == "1",
"Set APPLE_DEV_SKILLS_RUN_XCODEGEN_INTEGRATION=1 on macOS to run the XcodeGen integration test.",
)
def test_xcodegen_bootstrap_generates_catalog_symbols(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
code, payload = self.run_script(
"--name",
"TemplateProbe",
"--file-prefix",
"TMP",
"--destination",
tmpdir,
"--platform",
"macos",
"--project-generator",
"xcodegen",
"--skip-validation",
)
self.assertEqual(code, 0, payload)

target = Path(payload["resolved_path"])
project = target / "TemplateProbe.xcodeproj"
derived_data_path = Path(tmpdir) / "DerivedData"
build_settings = subprocess.run(
[
"xcodebuild",
"-showBuildSettings",
"-project",
str(project),
"-scheme",
"TemplateProbe",
"-derivedDataPath",
str(derived_data_path),
],
cwd=target,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(build_settings.returncode, 0, build_settings.stderr)
for setting in (
"ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES",
"LOCALIZATION_PREFERS_STRING_CATALOGS = YES",
"STRING_CATALOG_GENERATE_SYMBOLS = YES",
"SWIFT_EMIT_LOC_STRINGS = YES",
):
self.assertIn(setting, build_settings.stdout)

test_result = subprocess.run(
[
"xcodebuild",
"test",
"-project",
str(project),
"-scheme",
"TemplateProbe",
"-destination",
"platform=macOS,arch=arm64",

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 Select the host architecture for the macOS test

In the new opt-in XcodeGen integration test, hard-coding the destination to arch=arm64 makes APPLE_DEV_SKILLS_RUN_XCODEGEN_INTEGRATION=1 fail on Intel macOS hosts and Intel GitHub runners because they cannot execute the generated ARM test bundle. Omit the architecture so Xcode selects the host Mac, or derive it from the current machine.

Useful? React with 👍 / 👎.

"-derivedDataPath",
str(derived_data_path),
],
cwd=target,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(test_result.returncode, 0, test_result.stdout + test_result.stderr)

generated_sources = derived_data_path.glob(
"Build/Intermediates.noindex/TemplateProbe.build/**/DerivedSources"
)
generated_files = {
source.name
for directory in generated_sources
for source in directory.glob("Generated*.swift")
}
self.assertIn("GeneratedAssetSymbols.swift", generated_files)
self.assertIn("GeneratedStringSymbols_Localizable.swift", generated_files)

def test_xcodegen_templates_are_checked_in_as_bootstrap_sources(self) -> None:
expected_templates = {
"project.yml.tmpl",
Expand Down Expand Up @@ -399,6 +497,7 @@ def test_xcodegen_templates_are_checked_in_as_bootstrap_sources(self) -> None:
self.assertIn("- path: Sources", project_template)
self.assertIn("- path: Shared", project_template)
self.assertIn("- path: Tests", project_template)
self.assertEqual(project_template.count('- "**/.gitkeep"'), 2)
self.assertNotIn("- path: Sources/App", project_template)
self.assertNotIn("- path: Sources/Resources", project_template)
self.assertNotIn("- path: Sources/Support", project_template)
Expand Down
Loading