Back to Cherry Studio

BootConfigMigrator

src/main/data/migration/v2/migrators/README-BootConfigMigrator.md

2.0.08.5 KB
Original Source

BootConfigMigrator

The BootConfigMigrator migrates early-boot configuration from legacy storage into bootConfigService — the synchronous, file-based config used by code that runs before the lifecycle system takes over (e.g. Chromium flags, custom userData directory).

Unlike other migrators, it writes to a file-based store (~/.cherrystudio/boot-config.json) rather than a SQLite table. See Boot Config Overview for why this system exists.

Data Sources

Boot config pulls from five source kinds. Four are classification-driven (via data-classify + BOOT_CONFIG_*_MAPPINGS in mappings/BootConfigMappings.ts); one is manually maintained inline for a data shape the toolchain doesn't yet model.

Source kindReaderOriginCurrently migrates
reduxReduxStateReaderRedux Persist reduxData JSONsettings.disableHardwareAccelerationapp.disable_hardware_acceleration
electronStorectx.sources.electronStore (electron-store){userData}/config.json(none currently — classification empty)
dexie-settingsDexieSettingsReaderDexie settings table export(none currently — classification empty)
localStorageLocalStorageReaderlocalStorage export JSON(none currently — classification empty)
configfileLegacyHomeConfigReader~/.cherrystudio/config/config.json (v1 home config file)appDataPathapp.user_data_path

The configfile source

The configfile source exists because v1 stored the user-customized userData directory in ~/.cherrystudio/config/config.json rather than in any of the four classification-driven stores. That file is outside the app's userData directory (intentionally — it needs to be readable before the userData path is decided), so none of the other readers can reach it.

LegacyHomeConfigReader reads the v1 file and normalizes two historical data shapes:

  • Legacy string: { "appDataPath": "/path" } → wrapped into a single-entry record keyed by app.getPath('exe')
  • Array (current v1): { "appDataPath": [{ executablePath, dataPath }, ...] } → converted to Record<executablePath, dataPath>; entries missing either field are filtered out

Returns null (not {}) when no data is present (missing file / parse error / empty array / all entries invalid). This null flows into the shared null-skip guard in prepare(), matching the other sources' "no data → skip" semantics.

Field Mappings

Redux → BootConfig

Source (category / key)Target KeyTypeDefault
settings.disableHardwareAccelerationapp.disable_hardware_accelerationbooleanfalse

Config file → BootConfig

Source (file / field)Target KeyTypeDefault
~/.cherrystudio/config/config.jsonappDataPathapp.user_data_pathRecord<string, string>(null — see below)

Why defaultValue: null for config-file entries: the other sources fall back to DefaultBootConfig[targetKey] when the source has no value, so missing keys get sensible defaults. For config-file data like app.user_data_path, "no v1 file" must mean "nothing to migrate" — writing the schema default {} would be a spurious migration. Setting defaultValue: null on these entries routes them through the shared null-skip guard in prepare(), skipping the item entirely when the reader returns null.

Data Quality Handling

IssueDetectionHandling
v1 file missing!fs.existsSync(path) in readerReader returns null → migrator skips app.user_data_path
v1 file JSON parse errorJSON.parse throws in readerReader returns null → migrator skips
v1 file I/O errorfs.readFileSync throwsReader returns null → migrator skips
appDataPath field missingNot in parsed objectReader returns null → migrator skips
appDataPath wrong type (e.g. number)typeof !== 'string' && !Array.isArrayReader returns null → migrator skips
appDataPath: [] or array with all invalid entriesFiltered record has 0 keysReader returns null → migrator skips (C1 correctness — must not write {})
Redux source missing a keyreduxData path lookup returns undefinedFalls back to DefaultBootConfig[targetKey] (e.g. app.disable_hardware_accelerationfalse)
Schema-invalid value from any source (wrong type)bootConfigSchema.shape[targetKey].safeParse in prepare()Item skipped with a warning; migration continues (bootConfigService.set() would otherwise throw and fail the whole run)

Writes and Validation

  • Writes via bootConfigService.set(targetKey, value) followed by bootConfigService.persist() (the strict flush variant) to force an immediate durable write. persist() throws on a disk-write failure, which execute() catches and reports as { success: false, error } — so a failed write surfaces as a migration failure instead of a silent false-success.
  • validate() iterates preparedItems and checks bootConfigService.get(targetKey) !== undefined.
  • Known validate weakness: for Record<string, string> keys like app.user_data_path, mergeDefaults() fills a {} default on get, so value !== undefined is always true — validate() cannot confirm the written value is correct for Record-typed keys. (A failed disk write is now caught upstream: persist() throws and execute() reports failure.) Unit tests in __tests__/BootConfigMigrator.test.ts compensate by directly asserting bootConfigService.get('app.user_data_path') returns the expected structure rather than relying on validate().

Implementation Files

  • BootConfigMigrator.tsprepare/execute/validate phases; loadMigrationItems() merges classification-derived mappings (from BootConfigMappings.ts) with the inline configFileMappings local const.
  • ../utils/LegacyHomeConfigReader.ts — sync reader for v1 home config file; read-only; does not validate path accessibility of the returned dataPath values.
  • mappings/BootConfigMappings.ts — auto-generated mappings for the 4 classification-driven sources. The targetKey: BootConfigKey type annotation (emitted by generate-migration.js) provides the regen safety net: if a key is removed from the schema, mapping references fail to compile.
  • ../../../../../shared/data/bootConfig/bootConfigSchemas.ts — fully auto-generated schema (classification keys + MANUAL_BOOT_CONFIG_ITEMS from generate-boot-config.js). Single bootConfigSchema zod object (source of truth, validated at runtime), inferred BootConfigSchema type, single DefaultBootConfig const.

AppImage / Windows Portable Executable Path

On AppImage Linux and Windows portable builds, v1's init.ts:51-60 writes a special executablePath into config.json:

  • AppImage: path.dirname(APPIMAGE) + '/cherry-studio.appimage'
  • Windows portable: PORTABLE_EXECUTABLE_DIR + '/cherry-studio-portable.exe'

These differ from app.getPath('exe'). LegacyHomeConfigReader does NOT reproduce this normalization — array entries are migrated verbatim with their original executablePath key, and the legacy-string fallback uses raw app.getPath('exe').

Migration-time impact is resolved: resolveMigrationPaths() in core/MigrationPaths.ts performs its own legacy config detection using getNormalizedExecutablePath() (from userDataLocation.ts), which correctly normalizes AppImage/portable exe paths. This runs before the migration engine starts, ensuring the correct userData is used for all migration operations. LegacyHomeConfigReader still uses raw app.getPath('exe') for the BootConfig migration write, but the consumer side (resolveUserDataLocation()) also uses the normalized path for lookup, so both sides match for array-format entries. For string-format entries, LegacyHomeConfigReader keys by raw exe while the preboot lookup uses normalized exe — this mismatch is harmless because resolveMigrationPaths() has already pre-written the correct normalized-key entry to boot-config.json.

Code Quality

All implementation code includes detailed comments:

  • File-level comments: describe sources and write target
  • Type-level comments: explain why MigrationItem.targetKey is BootConfigKey (regen safety net) and why configFileMappings is inline rather than in an auto-generated file
  • Logic-level comments: explain the defaultValue: null semantic for config-file items (vs fallback-to-default for other sources)