Files

10 KiB
Executable File

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): 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.