From fc8f11eb96e4e793e187cd6f991cceb18e451bbb Mon Sep 17 00:00:00 2001 From: Iain Date: Sun, 4 Oct 2026 14:48:58 +0100 Subject: [PATCH 1/2] feat(data): let Google and Fairphone devices inherit the brand's unlock --- .github/CONTRIBUTING.md | 8 +++++--- data/brands.yml | 4 +++- data/devices/fairphone/fp2.yml | 4 +++- schema/brands.json | 1 + src/lib/data/model.js | 4 ++++ src/lib/table.js | 3 ++- src/pages/devices/[brand]/[slug].astro | 3 ++- tests/fixtures/good/brands.yml | 1 + tests/model.test.js | 16 +++++++++++++++- 9 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2897842..8274ca0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -82,12 +82,14 @@ A brand can carry its own `bootloader` block, for the policy that applies across - key: google name: Google bootloader: - notes: Pixel and Nexus phones from 2015 on unlock with fastboot, unless they were sold carrier locked. + unlock: conditional + notes: Pixel and Nexus phones unlock with fastboot, unless a carrier has SIM locked them. source: https://source.android.com/docs/setup/build/running ``` -It shows on the brand page and under Bootloader on every device page, and it never changes what a -device's own `unlock` says. Both fields are needed together, and only add one where the policy +It shows on the brand page and under Bootloader on every device page. A device whose own `unlock` +is `unknown` takes the brand's, marked as brand policy, and anything the device file says wins. +`notes` and `source` are needed together, `unlock` is optional, and only add one where the policy really does hold brand-wide - most brands split it by chipset, region or Android version, so they stay unknown. diff --git a/data/brands.yml b/data/brands.yml index dd095be..20ed926 100644 --- a/data/brands.yml +++ b/data/brands.yml @@ -16,6 +16,7 @@ - key: fairphone name: Fairphone bootloader: + unlock: "yes" notes: Fairphone publishes an unlock procedure for its phones, the Fairphone 2 aside. source: https://support.fairphone.com/hc/en-us/articles/10492476238865-How-to-unlock-or-lock-your-Fairphone-s-bootloader - key: fxtec @@ -25,7 +26,8 @@ - key: google name: Google bootloader: - notes: Pixel and Nexus phones from 2015 on unlock with fastboot, unless they were sold carrier locked. + unlock: conditional + notes: Pixel and Nexus phones unlock with fastboot, unless a carrier has SIM locked them. source: https://source.android.com/docs/setup/build/running - key: htc name: HTC diff --git a/data/devices/fairphone/fp2.yml b/data/devices/fairphone/fp2.yml index 8ab84c0..0e836e7 100644 --- a/data/devices/fairphone/fp2.yml +++ b/data/devices/fairphone/fp2.yml @@ -5,4 +5,6 @@ released: 2015 soc: Qualcomm MSM8974AB-AB Snapdragon 801 hardware: [cellular] bootloader: - unlock: unknown + unlock: "yes" + notes: Ships unlocked, so LineageOS goes straight on over fastboot with no unlock step. + source: https://wiki.lineageos.org/devices/FP2/install/ diff --git a/schema/brands.json b/schema/brands.json index e5ceca1..59c25c3 100644 --- a/schema/brands.json +++ b/schema/brands.json @@ -16,6 +16,7 @@ "required": ["notes", "source"], "additionalProperties": false, "properties": { + "unlock": { "enum": ["yes", "no", "conditional"] }, "notes": { "type": "string", "minLength": 1 }, "source": { "$ref": "#/$defs/link" } } diff --git a/src/lib/data/model.js b/src/lib/data/model.js index 0da3fb5..be91db3 100644 --- a/src/lib/data/model.js +++ b/src/lib/data/model.js @@ -28,8 +28,12 @@ export function buildModel(data = getData(), now = new Date()) { const brand = brands.get(brandKey); const prefixed = device.name.toLowerCase().startsWith(brand.name.toLowerCase()); const slug = device.slug ?? slugify(device.name); + const policy = device.bootloader.unlock === 'unknown' && brand.bootloader?.unlock; const entry = { ...device, + bootloader: policy + ? { ...device.bootloader, unlock: policy, origin: 'brand' } + : device.bootloader, key, file, brand, diff --git a/src/lib/table.js b/src/lib/table.js index e633ac5..7d7eb35 100644 --- a/src/lib/table.js +++ b/src/lib/table.js @@ -65,7 +65,7 @@ export const needsCell = ({ firmware, channel }) => { return chips.length ? { chips } : {}; }; -const suffixes = { upstream: 'docs', rom: 'ROM' }; +const suffixes = { upstream: 'docs', rom: 'ROM', brand: 'brand' }; export const originSuffix = ({ origin, inferred }) => [suffixes[origin], inferred && 'inferred'].filter(Boolean).join(', ') || undefined; @@ -181,6 +181,7 @@ export const deviceRows = (devices, roms) => unlock: { tone: lockTone[device.bootloader.unlock], text: labelOf(device.bootloader.unlock), + suffix: originSuffix(device.bootloader), }, ...Object.fromEntries(roms.map((rom) => [rom.key, romStatus(device, rom)])), }, diff --git a/src/pages/devices/[brand]/[slug].astro b/src/pages/devices/[brand]/[slug].astro index 3b39331..b090ece 100644 --- a/src/pages/devices/[brand]/[slug].astro +++ b/src/pages/devices/[brand]/[slug].astro @@ -27,6 +27,7 @@ const { device } = Astro.props; const { features } = getModel(); const { bootloader, stock, support } = device; const policy = device.brand.bootloader; +const unlock = `${labelOf(bootloader.unlock)}${bootloader.origin ? ', brand policy' : ''}`; const [current, ended] = splitByActive(support); const unreported = featuresFrom(features, support, 'none'); const quiet = new Set( @@ -138,7 +139,7 @@ const codename = device.codenames[0];

Bootloader

- + {bootloader.notes &&

{bootloader.notes}

} {bootloader.source && (

Going by {new URL(bootloader.source).hostname}.

diff --git a/tests/fixtures/good/brands.yml b/tests/fixtures/good/brands.yml index a590417..5275f70 100644 --- a/tests/fixtures/good/brands.yml +++ b/tests/fixtures/good/brands.yml @@ -1,5 +1,6 @@ - key: acme name: Acme bootloader: + unlock: "yes" notes: Acme phones unlock with fastboot. source: https://acme.test/unlock diff --git a/tests/model.test.js b/tests/model.test.js index c2906e5..bb5a062 100644 --- a/tests/model.test.js +++ b/tests/model.test.js @@ -7,6 +7,12 @@ import { resolveCell } from '../src/lib/data/status.js'; const model = (now) => buildModel(loadData('tests/fixtures/good'), new Date(now)); const rowOf = (now, rom) => model(now).support.find((row) => row.device.key === 'rocket' && row.rom.key === rom); +const unlockOf = (deviceUnlock, brandUnlock) => { + const data = loadData('tests/fixtures/good'); + data.devices[0].data.bootloader = { unlock: deviceUnlock }; + data.brands.data[0].bootloader.unlock = brandUnlock; + return buildModel(data).devices[0].bootloader; +}; const cellsOf = (row) => Object.fromEntries( Object.entries(row.cells).map(([key, cell]) => [key, [cell.value, cell.origin]]), @@ -21,7 +27,7 @@ test('cells resolve report, then docs, then hardware, then ROM-wide', () => { }); }); -test('a brand policy reaches the device without touching its own unlock', () => { +test("a device's own unlock beats the brand policy", () => { const [device] = model('2026-02-01').devices; assert.equal(device.brand.bootloader.source, 'https://acme.test/unlock'); assert.deepEqual(device.bootloader, { @@ -30,6 +36,14 @@ test('a brand policy reaches the device without touching its own unlock', () => }); }); +test('an unknown unlock takes the brand policy, marked as such', () => { + assert.deepEqual(unlockOf('unknown', 'yes'), { unlock: 'yes', origin: 'brand' }); +}); + +test('a brand without an unlock leaves the device unknown', () => { + assert.deepEqual(unlockOf('unknown', undefined), { unlock: 'unknown' }); +}); + test('explicit values beat inferred ones, whatever layer they sit in', () => { const feature = { key: 'wallet' }; const cell = resolveCell(feature, { From 3c99717edbd1128d007808277c1b580cd1c53986 Mon Sep 17 00:00:00 2001 From: Iain Date: Sun, 4 Oct 2026 14:51:49 +0100 Subject: [PATCH 2/2] fix(data): word the Google and FP2 unlock notes to match their sources --- .github/CONTRIBUTING.md | 2 +- data/brands.yml | 2 +- data/devices/fairphone/fp2.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8274ca0..505a189 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -83,7 +83,7 @@ A brand can carry its own `bootloader` block, for the policy that applies across name: Google bootloader: unlock: conditional - notes: Pixel and Nexus phones unlock with fastboot, unless a carrier has SIM locked them. + notes: Pixel and Nexus devices unlock with fastboot, unless a carrier has SIM locked them. source: https://source.android.com/docs/setup/build/running ``` diff --git a/data/brands.yml b/data/brands.yml index 20ed926..b80e514 100644 --- a/data/brands.yml +++ b/data/brands.yml @@ -27,7 +27,7 @@ name: Google bootloader: unlock: conditional - notes: Pixel and Nexus phones unlock with fastboot, unless a carrier has SIM locked them. + notes: Pixel and Nexus devices unlock with fastboot, unless a carrier has SIM locked them. source: https://source.android.com/docs/setup/build/running - key: htc name: HTC diff --git a/data/devices/fairphone/fp2.yml b/data/devices/fairphone/fp2.yml index 0e836e7..3df76aa 100644 --- a/data/devices/fairphone/fp2.yml +++ b/data/devices/fairphone/fp2.yml @@ -6,5 +6,5 @@ soc: Qualcomm MSM8974AB-AB Snapdragon 801 hardware: [cellular] bootloader: unlock: "yes" - notes: Ships unlocked, so LineageOS goes straight on over fastboot with no unlock step. + notes: LineageOS installs straight over fastboot, with no unlock step. source: https://wiki.lineageos.org/devices/FP2/install/