10 KiB
Executable File
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/DuckDuckGoby 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) compileAssets.carand generateImageResource/ColorResource, but notNSImage.foo/NSColor.foo/Color.fooextensions → ~575 call sites moved toNSImage(resource:)/NSColor(resource:)/Color(.foo)(found by building withASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS=NO). Nativeswift buildhandles neither. - Core Data models with
codeGenerationType="class"work in a package (Xcode and swiftbuild): entity classes are generated in the package module,.momdgoes to the package resource bundle → load withBundle.module. Don't keep a copy in the app target (duplicate@objcentity classes). - Xcode treats only a configuration named
Debugas debug for packages:CIbuilds packages as release (noDEBUG, no testability).SPM_FORCE_DEBUG=1(read byPackage.swift) +ENABLE_TESTABILITY=trueon 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 withoutSPM_FORCE_DEBUG, aCIbuild reuses the manifest withoutDEBUGuntilPackage.swiftchanges. Fresh CI runners are fine; locally editPackage.swiftor 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 inNavigationBarViewController). 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.swifthad.package(path: "../VPN")(doesn't exist; Xcode resolves it by identity through the workspace) — breaksswift build; fixed to../../../SharedPackages/VPN.- Building the host app without
-onlyUsePackageVersionsFromResolvedFilere-resolved packages and rewrote the projectPackage.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). Insteadsign-dylib.shsigns theswift builddylib and the frameworks it links with the host app's identity. swift build --build-system swiftbuild --product DuckDuckGoBrowserDynamicoutput:.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_FLAGSis empty for the app — onlyDEBUGremains (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.xcdatamodeldin 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:
InputFilesCheckerfails the build otherwise, so remove a file from both. InputFilesChecker.temporarilyAllowedMisplacedFileslists the files still outside their owning target, one block per stage; stale entries fail the build.- Local package products are linked as
XCSwiftPackageProductDependencywithout apackagekey. Unit Tests don't linkNetworkProtectionMacproducts; modules are visible through the host app. - Transitive module imports work in Xcode (e.g.
SubscriptioninsideDataBrokerProtection-macOSwithout a direct product).DataBrokerProtection-macOSrelies on this, so moved files follow the same convention. UserTextexists 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'sLocalizable.xcstrings.UserDefaultsWrapperis@deprecatedon main ("Use KeyedStoring pattern") — don't move it, follow that migration.SANDBOX_TEST_TOOL/ALPHAwere dead: packages never receive targetFEATURE_FLAGS, and sandbox-test-tool compiled only its own file. The system extension always hasAppVersion.runType == .normal.NETP_SYSTEM_EXTENSIONremains 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 rootDuckDuckGo.xcworkspacePackage.resolvedlackedswift-http-structured-headers(added by iOSSitePermissions), so workspace builds with-disableAutomaticPackageResolutionfail. - Offline builds: copy
SourcePackagesfrom 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, plusbuild-for-testingformacOS Unit Tests/macOS Integration Tests. - Run SwiftLint from
macOS/so the project.swiftlint.ymlapplies; running it from subfolders uses defaults and reports noise.
App Store UI tests (sandbox)
AppStoreSandboxUITestsreplaced sandbox-test-tool (#6980): custom downloads folder outside the auto-granted folders survives relaunch (security-scoped bookmark inDownloadsPreferences), and downloaded files stay accessible after relaunch (per-file bookmarks inDownloadListStore).- The App Store schemes default to
UI Tests App Store Sandbox.xctestplan(this class only);UI Tests App Store.xctestplanis 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 noAssets.car, so compiling is a second invocation (same as Xcode'sGenerateAssetSymbols+CompileAssetCatalog). - For package targets Xcode and
swiftbuildgenerateImageResource/ColorResourcesymbols but notNSImage.foo/NSColor.fooextensions. 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.carwithoutCFBundleIdentifier. - 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.jsondependsOnhas no up-to-date check → needs a gate script (input hash viagit ls-files -s+ diff, stamp file).xcodebuildalways 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 withswift buildand 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; PlugInsNetworkProtectionAppExtension.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. Usexcrun swift, and setswift.pathin VSCode to Xcode's toolchain.- Native builds rewrite stale
Package.resolvedfiles (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: emptyDBP.xcassetsinside.copy("BundleResources")— dead, never compiled.