Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions data/brands.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions schema/brands.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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?://" }
}
}
6 changes: 4 additions & 2 deletions schema/device.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" } }
Expand All @@ -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?://" }
}
}
2 changes: 1 addition & 1 deletion schema/rom.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@
"$defs": {
"link": {
"type": "string",
"format": "uri",
"format": "link",
"pattern": "^https?://"
},
"key": {
Expand Down
2 changes: 1 addition & 1 deletion schema/upstream.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@
}
]
},
"link": { "type": "string", "format": "uri", "pattern": "^https?://" }
"link": { "type": "string", "format": "link", "pattern": "^https?://" }
}
}
2 changes: 2 additions & 0 deletions src/lib/data/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand Down
10 changes: 10 additions & 0 deletions src/pages/devices/[brand]/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -139,6 +140,15 @@ const codename = device.codenames[0];
<h2 id="bootloader">Bootloader</h2>
<Facts items={[{ label: 'Unlock', value: labelOf(bootloader.unlock) }]} />
{bootloader.notes && <p>{bootloader.notes}</p>}
{bootloader.source && (
<p>Going by <a href={bootloader.source}>{new URL(bootloader.source).hostname}</a>.</p>
)}
{policy && (
<p>
{device.brand.name}'s policy: {policy.notes} Going by{' '}
<a href={policy.source}>{new URL(policy.source).hostname}</a>.
</p>
)}
</section>

<section aria-labelledby="roms">
Expand Down
6 changes: 6 additions & 0 deletions src/pages/devices/[brand]/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ const romNames = [
<a href={brand.website}>{new URL(brand.website).hostname}</a>
</p>
)}
{brand.bootloader && (
<p>
{brand.bootloader.notes} Going by{' '}
<a href={brand.bootloader.source}>{new URL(brand.bootloader.source).hostname}</a>.
</p>
)}
</section>
<DeviceTables
id="brand-devices"
Expand Down
5 changes: 5 additions & 0 deletions tests/check.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ test('schema errors read like English', () => {
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/);
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/bad/brands.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/fixtures/good/brands.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
- key: acme
name: Acme
bootloader:
notes: Acme phones unlock with fastboot.
source: https://acme.test/unlock
9 changes: 9 additions & 0 deletions tests/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
Loading