# VSCode Migration — Technical Findings Back to [[projects/vscode-migration/vscode-migration]]. Verified on Xcode 26.6 / Swift 6.3.3, Sep 2026. ## Browser package (Xcode 27.2, Oct 2026) - The project targets Xcode 27 now; Xcode 26 isn't a constraint any more. - A package folder can overlap project groups: other targets keep compiling files inside `macOS/DuckDuckGo` by path, and the app target keeps resource file references there (verified with a small xcodegen project and the real app). - Asset catalogs in a package: Xcode (26.6 and 27.2) and `swift build --build-system swiftbuild` (27.2) compile `Assets.car` and generate `ImageResource`/`ColorResource`, but **not** `NSImage.foo`/`NSColor.foo`/`Color.foo` extensions → ~575 call sites moved to `NSImage(resource:)` / `NSColor(resource:)` / `Color(.foo)` (found by building with `ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS=NO`). Native `swift build` handles neither. - Core Data models with `codeGenerationType="class"` work in a package (Xcode and swiftbuild): entity classes are generated in the package module, `.momd` goes to the package resource bundle → load with `Bundle.module`. Don't keep a copy in the app target (duplicate `@objc` entity classes). - Xcode treats only a configuration named `Debug` as debug for packages: `CI` builds packages as release (no `DEBUG`, no testability). `SPM_FORCE_DEBUG=1` (read by `Package.swift`) + `ENABLE_TESTABILITY=true` on the command line fix it; command-line build settings reach package targets, project-level settings don't. - SwiftPM caches evaluated manifests by content in `~/Library/Caches/org.swift.swiftpm/manifests`, not by environment: after building without `SPM_FORCE_DEBUG`, a `CI` build reuses the manifest without `DEBUG` until `Package.swift` changes. Fresh CI runners are fine; locally edit `Package.swift` or clear the cache. - Storyboard objects with `customModuleProvider="target"` resolve to the module of the target compiling the storyboard → after moving the code into a package, classes weren't found (crash in `NavigationBarViewController`). Moot now that the last storyboard is dropped. - Transitive modules used only through extensions (e.g. Punycode's `String.idnaDecoded`) link in Debug but not in release configurations → declare them as explicit package dependencies. - `DataBrokerProtection-macOS/Package.swift` had `.package(path: "../VPN")` (doesn't exist; Xcode resolves it by identity through the workspace) — breaks `swift build`; fixed to `../../../SharedPackages/VPN`. - Building the host app without `-onlyUsePackageVersionsFromResolvedFile` re-resolved packages and rewrote the project `Package.resolved` (`xctest-dynamic-overlay` → `swift-issue-reporting`). - VSCode host app: the app has system extension entitlements → the hardened runtime refuses `disable-library-validation` ("Hardened Runtime relaxation entitlements disallowed on System Extensions", process killed at exec). Instead `sign-dylib.sh` signs the `swift build` dylib and the frameworks it links with the host app's identity. - `swift build --build-system swiftbuild --product DuckDuckGoBrowserDynamic` output: `.build/out/Products/Debug/libDuckDuckGoBrowserDynamic.dylib`; links GRDB, Sparkle, OpenSSL, Lottie frameworks next to it. At runtime the dylib finds its resource bundle in the host app (`Contents/Resources/DuckDuckGoBrowser_DuckDuckGo_Privacy_Browser.bundle`). - Concurrency warnings ("error in the Swift 6 language mode") are the same in the old app target and in the package on Xcode 27 — existing debt, not caused by the move. ## App target readiness - DMG and App Store targets compile identical sources (1398 files); `FEATURE_FLAGS` is empty for the app — only `DEBUG` remains (SPM handles it). - No ObjC/C in `macOS/DuckDuckGo/`, empty bridging header. - Unit Tests compile only 2 app files directly; the rest is `@testable import` → can switch to the library. - 38 app files (by path; an earlier name-based count said 46) were shared with helper targets. After the membership trim 27 remained; after stages A+B 18. Progress per stage: [[projects/vscode-migration/vscode-migration#Target split stages]]. - Remaining Xcode-only resources: storyboards `PasswordManager`, `BookmarksBar`, `NavigationBar`, `Feedback` + `BookmarksBarCollectionViewItem.xib`; 8 `.xcdatamodeld` in the app, 3 in BSK. ## Target split (files shared with helper targets) - Find out which target really needs a file by removing it from the target and building, not by grepping: 15 of 38 memberships turned out unused (some only transitively needed, e.g. TipKit, Safari/WebKit version readers). - DMG and App Store variants must have identical sources: `InputFilesChecker` fails the build otherwise, so remove a file from both. - `InputFilesChecker.temporarilyAllowedMisplacedFiles` lists the files still outside their owning target, one block per stage; stale entries fail the build. - Local package products are linked as `XCSwiftPackageProductDependency` without a `package` key. Unit Tests don't link `NetworkProtectionMac` products; modules are visible through the host app. - Transitive module imports work in Xcode (e.g. `Subscription` inside `DataBrokerProtection-macOS` without a direct product). `DataBrokerProtection-macOS` relies on this, so moved files follow the same convention. - `UserText` exists twice (app and VPN agent, `DuckDuckGoVPN/UserText.swift`); shared files extend whichever one the target compiles. Moving them into a package means moving the strings (and translations) to the package's `Localizable.xcstrings`. - `UserDefaultsWrapper` is `@deprecated` on main ("Use KeyedStoring pattern") — don't move it, follow that migration. - `SANDBOX_TEST_TOOL` / `ALPHA` were dead: packages never receive target `FEATURE_FLAGS`, and sandbox-test-tool compiled only its own file. The system extension always has `AppVersion.runType == .normal`. - `NETP_SYSTEM_EXTENSION` remains only in extension-only code (keychain service names, defaults, pixel source). ## Build verification (offline) - Build with `-project macOS/DuckDuckGo-macOS.xcodeproj` (as CI does), not the root workspace: on main (late Sep 2026) the root `DuckDuckGo.xcworkspace` `Package.resolved` lacked `swift-http-structured-headers` (added by iOS `SitePermissions`), so workspace builds with `-disableAutomaticPackageResolution` fail. - Offline builds: copy `SourcePackages` from a DerivedData whose checkouts contain all pinned revisions, then `-disableAutomaticPackageResolution -skipPackageUpdates -onlyUsePackageVersionsFromResolvedFile -skipPackagePluginValidation -skipMacroValidation CODE_SIGNING_ALLOWED=NO`. - Schemes covering all helper targets: `macOS Browser`, `macOS Browser App Store`, `DuckDuckGoVPNSysexAppStore`, `macOS VPN Proxy Extension`, `macOS VPN System Extension`, `macOS VPN Agent App Store`, `macOS PIR Agent`, plus `build-for-testing` for `macOS Unit Tests` / `macOS Integration Tests`. - Run SwiftLint from `macOS/` so the project `.swiftlint.yml` applies; running it from subfolders uses defaults and reports noise. ## App Store UI tests (sandbox) - `AppStoreSandboxUITests` replaced sandbox-test-tool ([#6980](https://github.com/duckduckgo/apple-browsers/pull/6980)): custom downloads folder outside the auto-granted folders survives relaunch (security-scoped bookmark in `DownloadsPreferences`), and downloaded files stay accessible after relaunch (per-file bookmarks in `DownloadListStore`). - The App Store schemes default to `UI Tests App Store Sandbox.xctestplan` (this class only); `UI Tests App Store.xctestplan` is used when CI filters by class name. CI runs the class along every full DMG run; failures create "GH Workflow Failure - App Store UI Tests". ## Asset catalogs - Xcode generates asset symbols with `actool --bundle-identifier … --generate-swift-asset-symbols X.swift`. In that mode actool writes no `Assets.car`, so compiling is a second invocation (same as Xcode's `GenerateAssetSymbols` + `CompileAssetCatalog`). - For **package** targets Xcode and `swiftbuild` generate `ImageResource`/`ColorResource` symbols but **not** `NSImage.foo` / `NSColor.foo` extensions. The app target uses extensions (e.g. `NSImage.menuVertical16`) → those call sites must change when moving into a package. - Native `swift build` (also what SourceKit-LSP uses) neither compiles catalogs nor generates symbols. - Native SwiftPM resource bundles are flat and lack Info.plist; CoreUI ignores `Assets.car` without `CFBundleIdentifier`. - In Xcode's plugin sandbox actool fails for iOS ("No available simulator runtimes"); macOS works. So a plugin can't replace Xcode's processing on iOS. - Plugin environment is identical for native and `swiftbuild` → can't tell them apart; Xcode is detectable by the `/BuildToolPluginIntermediates/` work dir. - Asset usage in the app catalog: ~415 via symbols, ~57 via string names, 20 only from storyboards, ~300 apparently unreferenced (heuristic). ## Extensions in VSCode tasks - `tasks.json` `dependsOn` has no up-to-date check → needs a gate script (input hash via `git ls-files -s` + diff, stamp file). - `xcodebuild` always visits explicit target dependencies (planning, Provisional Inputs) even when nothing changed; no flag to skip. Gating extensions only pays off once the app is built with `swift build` and assembled by a script. - Assemble: copy pre-signed extensions, sign the outer app last without `--deep`; same Team ID; embed provisioning profiles for NetworkExtension entitlements; system extension activates only from `/Applications`. - Embedded: Login Items `DuckDuckGo VPN.app`, `DuckDuckGo Personal Information Removal.app`; PlugIns `NetworkProtectionAppExtension.appex`, `VPNProxyExtension.appex`. ## Toolchain - `~/.swiftly/bin/swift` (6.2) is first in PATH; it can't compile packages importing WebKit against the macOS 26 SDK. Use `xcrun swift`, and set `swift.path` in VSCode to Xcode's toolchain. - Native builds rewrite stale `Package.resolved` files (e.g. `SharedPackages/Onboarding`) — don't commit those. ## Pre-existing issues found - `UIComponentsTests`: macOS 15 API without `@available` (package min 12.3). - `PerformanceTestTests`: reference removed types (`CoreWebVitalsAssessment`, `averageResourceSize`). - `DataBrokerProtectionCore`: empty `DBP.xcassets` inside `.copy("BundleResources")` — dead, never compiled.