diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2eb3e77..2897842 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -72,10 +72,25 @@ bootloader: notes: Carrier models, like Verizon's, can't be unlocked. ``` -Add `aliases` for model numbers and other names people search for. `type` is `tablet` or `handheld`, and left out for phones. `hardware` lists the bits that some features need, so a phone without NFC shows NFC as n/a. `bootloader.unlock` is `yes`, `no`, `conditional` (add `notes` saying when) or `unknown`. +Add `aliases` for model numbers and other names people search for. `type` is `tablet` or `handheld`, and left out for phones. `hardware` lists the bits that some features need, so a phone without NFC shows NFC as n/a. `bootloader.unlock` is `yes`, `no`, `conditional` (add `notes` saying when) or `unknown`, and `bootloader.source` links where you read it. The import only ever fills gaps in a device file, so your edits stay put. If the brand's new, add it to `data/brands.yml` too. +A brand can carry its own `bootloader` block, for the policy that applies across its phones: + +```yaml +- key: google + name: Google + bootloader: + notes: Pixel and Nexus phones from 2015 on unlock with fastboot, unless they were sold carrier locked. + 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 +really does hold brand-wide - most brands split it by chipset, region or Android version, so they +stay unknown. + --- ## Adding support for a ROM diff --git a/data/brands.yml b/data/brands.yml index 3ddbcee..dd095be 100644 --- a/data/brands.yml +++ b/data/brands.yml @@ -15,12 +15,18 @@ name: Essential - key: fairphone name: Fairphone + bootloader: + 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 name: F(x)tec - key: gigaset name: Gigaset - key: google name: Google + bootloader: + notes: Pixel and Nexus phones from 2015 on unlock with fastboot, unless they were sold carrier locked. + source: https://source.android.com/docs/setup/build/running - key: htc name: HTC - key: huawei diff --git a/schema/brands.json b/schema/brands.json index 1adf1a5..e5ceca1 100644 --- a/schema/brands.json +++ b/schema/brands.json @@ -10,7 +10,16 @@ "properties": { "key": { "$ref": "#/$defs/key" }, "name": { "type": "string", "minLength": 1 }, - "website": { "$ref": "#/$defs/link" } + "website": { "$ref": "#/$defs/link" }, + "bootloader": { + "type": "object", + "required": ["notes", "source"], + "additionalProperties": false, + "properties": { + "notes": { "type": "string", "minLength": 1 }, + "source": { "$ref": "#/$defs/link" } + } + } } }, "$defs": { @@ -19,6 +28,6 @@ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "description": "lowercase letters, numbers and hyphens" }, - "link": { "type": "string", "format": "uri", "pattern": "^https?://" } + "link": { "type": "string", "format": "link", "pattern": "^https?://" } } } diff --git a/schema/device.json b/schema/device.json index bb41d30..4451e5e 100644 --- a/schema/device.json +++ b/schema/device.json @@ -36,7 +36,8 @@ "additionalProperties": false, "properties": { "unlock": { "enum": ["yes", "no", "conditional", "unknown"] }, - "notes": { "type": "string", "minLength": 1 } + "notes": { "type": "string", "minLength": 1 }, + "source": { "$ref": "#/$defs/link" } }, "if": { "properties": { "unlock": { "const": "conditional" } } @@ -54,6 +55,7 @@ "type": "string", "pattern": "^[a-z0-9]+([_-][a-z0-9]+)*$", "description": "lowercase letters, numbers, hyphens and underscores" - } + }, + "link": { "type": "string", "format": "link", "pattern": "^https?://" } } } diff --git a/schema/rom.json b/schema/rom.json index 12e2768..07681cc 100644 --- a/schema/rom.json +++ b/schema/rom.json @@ -219,7 +219,7 @@ "$defs": { "link": { "type": "string", - "format": "uri", + "format": "link", "pattern": "^https?://" }, "key": { diff --git a/schema/upstream.json b/schema/upstream.json index 83faf51..8781d28 100644 --- a/schema/upstream.json +++ b/schema/upstream.json @@ -75,6 +75,6 @@ } ] }, - "link": { "type": "string", "format": "uri", "pattern": "^https?://" } + "link": { "type": "string", "format": "link", "pattern": "^https?://" } } } diff --git a/src/lib/data/check.js b/src/lib/data/check.js index 393bb44..f2e3e68 100644 --- a/src/lib/data/check.js +++ b/src/lib/data/check.js @@ -8,6 +8,8 @@ import { NEEDS_NOTE, allowedValues, cellValue } from './status.js'; const SCHEMAS = ['brands', 'features', 'rom', 'device', 'upstream', 'stock', 'support']; const ajv = addFormats(new Ajv({ allErrors: true, verbose: true })); +// the pages read a hostname off these, and ajv's uri format is looser than URL is +ajv.addFormat('link', (value) => URL.canParse(value)); for (const name of SCHEMAS) { ajv.addSchema( JSON.parse(readFileSync(new URL(`../../../schema/${name}.json`, import.meta.url), 'utf8')), diff --git a/src/pages/devices/[brand]/[slug].astro b/src/pages/devices/[brand]/[slug].astro index a49f23a..3b39331 100644 --- a/src/pages/devices/[brand]/[slug].astro +++ b/src/pages/devices/[brand]/[slug].astro @@ -26,6 +26,7 @@ export const getStaticPaths = () => const { device } = Astro.props; const { features } = getModel(); const { bootloader, stock, support } = device; +const policy = device.brand.bootloader; const [current, ended] = splitByActive(support); const unreported = featuresFrom(features, support, 'none'); const quiet = new Set( @@ -139,6 +140,15 @@ const codename = device.codenames[0];

Bootloader

{bootloader.notes &&

{bootloader.notes}

} + {bootloader.source && ( +

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

+ )} + {policy && ( +

+ {device.brand.name}'s policy: {policy.notes} Going by{' '} + {new URL(policy.source).hostname}. +

+ )}
diff --git a/src/pages/devices/[brand]/index.astro b/src/pages/devices/[brand]/index.astro index 3385187..e9275d6 100644 --- a/src/pages/devices/[brand]/index.astro +++ b/src/pages/devices/[brand]/index.astro @@ -29,6 +29,12 @@ const romNames = [ {new URL(brand.website).hostname}

)} + {brand.bootloader && ( +

+ {brand.bootloader.notes} Going by{' '} + {new URL(brand.bootloader.source).hostname}. +

+ )}
{ expectProblem(/imported: "2026-02-30" isn't a real date/); }); +test('a brand policy has to say where it came from, and link somewhere real', () => { + expectProblem(/^brands\.yml \[0\]\.bootloader\.source: is missing/); + expectProblem(/^brands\.yml \[1\]\.bootloader\.source: .* isn't a full link/); +}); + test('codenames, aliases and web addresses must be unique', () => { expectProblem(/rocket\.yml codenames\[0\]: "rocket_eea" should match the file name/); expectProblem(/rocket\.yml aliases\[0\]: "rk-2" is already used/); diff --git a/tests/fixtures/bad/brands.yml b/tests/fixtures/bad/brands.yml index a8cbf3b..1510fa9 100644 --- a/tests/fixtures/bad/brands.yml +++ b/tests/fixtures/bad/brands.yml @@ -1,4 +1,9 @@ - key: acme name: Acme + bootloader: + notes: Acme phones unlock with fastboot. - key: tidy name: Tidy + bootloader: + notes: Tidy phones don't unlock. + source: https://example.com:99999 diff --git a/tests/fixtures/good/brands.yml b/tests/fixtures/good/brands.yml index 23a5e91..a590417 100644 --- a/tests/fixtures/good/brands.yml +++ b/tests/fixtures/good/brands.yml @@ -1,2 +1,5 @@ - key: acme name: Acme + bootloader: + notes: Acme phones unlock with fastboot. + source: https://acme.test/unlock diff --git a/tests/model.test.js b/tests/model.test.js index a77e5f2..c2906e5 100644 --- a/tests/model.test.js +++ b/tests/model.test.js @@ -21,6 +21,15 @@ test('cells resolve report, then docs, then hardware, then ROM-wide', () => { }); }); +test('a brand policy reaches the device without touching its own unlock', () => { + const [device] = model('2026-02-01').devices; + assert.equal(device.brand.bootloader.source, 'https://acme.test/unlock'); + assert.deepEqual(device.bootloader, { + unlock: 'conditional', + notes: 'Carrier models are locked.', + }); +}); + test('explicit values beat inferred ones, whatever layer they sit in', () => { const feature = { key: 'wallet' }; const cell = resolveCell(feature, {