cefbuilds: Add installer CDN query file generation (see #4093)

Add cef_installer_builder.py to generate per-milestone and per-platform
JSON manifests consumed by the CEF bootstrap installer. Extend
cef_json_builder.py with abi_hash, signed archive tracking, and
revocation list (revoked.json) support. Includes unit tests.
This commit is contained in:
Marshall Greenblatt committed 2026-07-19 14:58:46 -04:00
1 parent 803fe341df
commit 7324f016a3
7 files changed
+1212 -60

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
# CEF Builds JSON Tools
Tools for generating and maintaining the JSON index files used by the
[CEF Builds](https://cef-builds.spotifycdn.com/index.html) CDN and the CEF
installer.
## Files
### Core libraries
- **`cef_json_builder.py`** — Builds and maintains the master `index.json` file
that contains the full catalog of CEF binary distributions across all
platforms, versions, and channels. Handles file metadata (name, size, sha1,
last_modified), Chromium version mapping, and sandbox compatibility hashes.
- **`cef_installer_builder.py`** — Generates installer-optimized CDN query files
from a populated `cef_json_builder` instance. Produces lightweight JSON files
designed for efficient HTTP HEAD timestamp checks by the CEF installer.
### Data files
- **`revoked.json`** — Manually maintained list of revoked CEF versions. Revoked
versions are excluded from all installer-generated JSON output. The installer
also fetches this file from the CDN and refuses to install any listed version.
### Examples and tests
- **`cef_json_builder_example.py`** — Example script demonstrating
`cef_json_builder` usage. Creates and updates a local `index.json` with fake
file data.
- **`cef_json_builder_test.py`** — Unit tests for `cef_json_builder`.
- **`cef_installer_builder_test.py`** — Unit tests for `cef_installer_builder`.
## CDN file formats
### Master index
- **`index.json`** — Full data index of all CEF binary distributions. Generated
by `cef_json_builder`. Keyed by platform, with version records sorted
newest-first by file last_modified timestamp.
### Installer query files
Generated by `cef_installer_builder` from the master index:
- **`stable.txt`** — Current stable milestone number (e.g., `137`).
- **`{milestone}.json`** (e.g., `137.json`) — Newest build per platform for a
given milestone. Used by the installer to check if an update is available.
- **`{milestone}_{platform}.json`** (e.g., `137_windows64.json`) — All builds
for a milestone+platform combination, sorted newest-first.
- **`{abi_hash}_{platform}.json`** (e.g., `abc123def4567890_windows64.json`) —
All builds compatible with a given ABI hash and platform across all milestones,
sorted newest-first. Windows platforms only (tied to `chrome_elf.dll` /
Chromium sandbox ABI).
## Concepts
### Platforms
Supported platforms: `linux32`, `linux64`, `linuxarm`, `linuxarm64`,
`macosarm64`, `macosx64`, `windows32`, `windows64`, `windowsarm64`.
### Distribution types
Each version may have files of these types: `standard`, `minimal`, `client`,
`tools`, `release_symbols`, `debug_symbols`, `signed`.
The `signed` type contains code-signed archives with catalog files, used by the
installer for verified installation.
### Channels
Versions belong to either the `stable` or `beta` channel.
### Version formats
- **New-style**: `137.3.5+g62d140e+chromium-137.0.7204.6`
- Short version: `137.3.5` (everything before the first `+`)
- Milestone: `137` (major version number)
- **Old-style**: `3.2704.1414.g185cd6c` (predates the installer format)
### ABI hash (`sandbox_compat`)
A 16-character hex string identifying the Windows sandbox ABI compatibility
level. Derived from `cef_api_versions.json` on GitHub. Versions sharing the same
ABI hash can use the same `chrome_elf.dll` and are interchangeable from the
sandbox's perspective.
### Revocation
The `revoked.json` file lists versions that must not be installed. When passed
to `cef_installer_builder`, revoked versions are automatically excluded from
all generated output. The installer also fetches this file from the CDN at
runtime and refuses to install any listed version.
Entries can revoke a single version or a range of versions:
```json
{
"revoked_versions": [
{
"version": "137.1.0",
"reason": "CVE-2024-XXXXX: critical sandbox escape",
"revoked_at": "2024-01-15T00:00:00Z"
},
{
"version_min": "138.0.0",
"version_max": "138.2.99",
"reason": "CVE-2024-YYYYY: affects all 138.0–138.2 builds",
"revoked_at": "2024-02-01T00:00:00Z"
}
]
}
```
**Single version** (point revocation):
- `version` — Short version string (e.g., `137.1.0`).
**Version range** (inclusive):
- `version_min` — Inclusive lower bound (e.g., `138.0.0`).
- `version_max` — Inclusive upper bound (e.g., `138.2.99`).
**Common fields:**
- `reason` — Human-readable explanation (CVE ID, bug reference, etc.).
- `revoked_at` — ISO 8601 timestamp of when the revocation was added.
A single-version entry using `version` is equivalent to setting both
`version_min` and `version_max` to the same value. If both `version` and
`version_min`/`version_max` are present, the range fields take precedence.
## Running tests
```bash
cd cef/tools/cefbuilds
python3 -m unittest cef_json_builder_test -v
python3 -m unittest cef_installer_builder_test -v
```
+235
View File
@@ -0,0 +1,235 @@
# Copyright (c) 2026 The Chromium Embedded Framework Authors. All rights
# reserved. Use of this source code is governed by a BSD-style license that
# can be found in the LICENSE file.
from __future__ import absolute_import
from __future__ import print_function
import json
from cef_json_builder import cef_json_builder, cef_json_encoder
class cef_installer_builder:
"""Generates installer-specific CDN query files from cef_json_builder data."""
_WINDOWS_PLATFORMS = ('windows32', 'windows64', 'windowsarm64')
def __init__(self, builder, revoked_json=None):
"""Create a new cef_installer_builder.
Args:
builder: A populated cef_json_builder instance.
revoked_json: Optional parsed revoked.json dict (result of json.loads()).
When provided, revoked versions are excluded from all generated output.
"""
self._builder = builder
# Trigger version sorting (normally only done on __repr__).
self._builder._sort_versions()
self._revoked_points = set()
self._revoked_ranges = []
if revoked_json is not None:
for entry in revoked_json.get('revoked_versions', []):
if 'version_min' in entry and 'version_max' in entry:
self._revoked_ranges.append(
(entry['version_min'], entry['version_max']))
elif 'version' in entry:
self._revoked_points.add(entry['version'])
@staticmethod
def _parse_version_tuple(version_str):
"""Parse '137.3.5' into (137, 3, 5) for comparison.
Pads with zeros so '137' becomes (137, 0, 0) and '137.4' becomes
(137, 4, 0).
"""
parts = [int(x) for x in version_str.split('.')]
while len(parts) < 3:
parts.append(0)
return tuple(parts)
def _is_revoked(self, short_version):
"""Returns True if the short version is revoked (point or range)."""
if short_version in self._revoked_points:
return True
v = self._parse_version_tuple(short_version)
for vmin, vmax in self._revoked_ranges:
if (self._parse_version_tuple(vmin) <= v <=
self._parse_version_tuple(vmax)):
return True
return False
def _get_installer_entry(self, version_obj):
"""Build one installer entry from a version record.
Returns None if the version has no signed-type file or is revoked.
"""
cef_version = version_obj['cef_version']
short_version = cef_json_builder.get_short_version(cef_version)
if self._is_revoked(short_version):
return None
# Find the signed file.
signed_file = None
for f in version_obj['files']:
if f['type'] == 'signed':
signed_file = f
break
if signed_file is None:
return None
entry = {
'version': short_version,
'file': signed_file['name'],
'sha1': signed_file['sha1'],
'last_modified': signed_file['last_modified'],
}
sandbox_compat = version_obj.get('sandbox_compat')
if sandbox_compat is not None:
entry['abi_hash'] = sandbox_compat
return entry
@staticmethod
def _to_json(result, prettyprint):
"""Serialize result to a JSON string with sorted keys."""
if prettyprint:
return json.dumps(result,
cls=cef_json_encoder,
sort_keys=True,
indent=2,
separators=(',', ': '))
return json.dumps(result, cls=cef_json_encoder, sort_keys=True)
def get_stable_milestone(self, channel='stable'):
"""Returns the highest milestone integer in the given channel.
Returns None if no versions exist.
"""
highest = None
for platform in self._builder.get_platforms():
for version_obj in self._builder.get_versions(platform):
if version_obj.get('channel', 'stable') != channel:
continue
milestone = cef_json_builder.get_milestone(version_obj['cef_version'])
if highest is None or milestone > highest:
highest = milestone
return highest
def generate_stable_txt(self, channel='stable'):
"""Returns string content for stable.txt (just the milestone number).
Returns None if no versions exist.
"""
milestone = self.get_stable_milestone(channel)
if milestone is None:
return None
return str(milestone)
def generate_milestone_json(self,
milestone,
channel='stable',
prettyprint=False):
"""Returns JSON for {milestone}.json — newest version per platform.
Returns '{}' when no platforms qualify.
"""
result = {}
for platform in self._builder.get_platforms():
for version_obj in self._builder.get_versions(platform):
if version_obj.get('channel', 'stable') != channel:
continue
if cef_json_builder.get_milestone(
version_obj['cef_version']) != milestone:
continue
entry = self._get_installer_entry(version_obj)
if entry is not None:
# First matching version is newest (sorted by last_modified).
result[platform] = entry
break
return self._to_json(result, prettyprint)
def generate_milestone_platform_json(self,
milestone,
platform,
channel='stable',
prettyprint=False):
"""Returns JSON for {milestone}_{platform}.json — all versions for a
milestone+platform, sorted newest-first.
Returns '[]' for unknown milestone/platform combinations.
"""
result = []
for version_obj in self._builder.get_versions(platform):
if version_obj.get('channel', 'stable') != channel:
continue
if cef_json_builder.get_milestone(
version_obj['cef_version']) != milestone:
continue
entry = self._get_installer_entry(version_obj)
if entry is not None:
result.append(entry)
return self._to_json(result, prettyprint)
def generate_abi_hash_platform_json(self,
abi_hash,
platform,
channel='stable',
prettyprint=False):
"""Returns JSON for {abi_hash}_{platform}.json — all builds matching
abi_hash for a platform across all milestones, sorted newest-first.
Only applicable to Windows platforms. Returns '[]' for non-Windows.
"""
if platform not in self._WINDOWS_PLATFORMS:
return '[]'
result = []
for version_obj in self._builder.get_versions(platform):
if version_obj.get('channel', 'stable') != channel:
continue
if version_obj.get('sandbox_compat') != abi_hash:
continue
entry = self._get_installer_entry(version_obj)
if entry is not None:
result.append(entry)
return self._to_json(result, prettyprint)
def get_abi_hashes(self, platform=None, channel='stable'):
"""Returns the set of distinct sandbox_compat values.
Args:
platform: Optional platform filter. Non-Windows platforms return empty.
channel: Channel filter (default 'stable').
"""
if platform is not None and platform not in self._WINDOWS_PLATFORMS:
return set()
platforms = [platform] if platform else self._builder.get_platforms()
hashes = set()
for p in platforms:
if p not in self._WINDOWS_PLATFORMS:
continue
for version_obj in self._builder.get_versions(p):
if version_obj.get('channel', 'stable') != channel:
continue
sc = version_obj.get('sandbox_compat')
if sc is not None:
hashes.add(sc)
return hashes
def get_milestones(self, channel='stable'):
"""Returns sorted list of all milestone integers in the given channel."""
milestones = set()
for platform in self._builder.get_platforms():
for version_obj in self._builder.get_versions(platform):
if version_obj.get('channel', 'stable') != channel:
continue
milestones.add(
cef_json_builder.get_milestone(version_obj['cef_version']))
return sorted(milestones)
@@ -0,0 +1,618 @@
# Copyright (c) 2026 The Chromium Embedded Framework Authors. All rights
# reserved. Use of this source code is governed by a BSD-style license that
# can be found in the LICENSE file.
from __future__ import absolute_import
from cef_json_builder import cef_json_builder
from cef_installer_builder import cef_installer_builder
import datetime
import json
import unittest
class TestCefInstallerBuilder(unittest.TestCase):
def setUp(self):
# Allow sandbox_compat queries for all milestones in tests.
self._orig_min = cef_json_builder.MIN_SANDBOX_COMPAT_MILESTONE
cef_json_builder.MIN_SANDBOX_COMPAT_MILESTONE = 0
def tearDown(self):
cef_json_builder.MIN_SANDBOX_COMPAT_MILESTONE = self._orig_min
# Helper to create a builder pre-populated with version data.
# Avoids remote queries by setting chromium_version and sandbox_compat.
def _create_builder(self):
return cef_json_builder(silent=True)
def _add_version(self,
builder,
platform,
cef_version,
chromium_version,
types=None,
channel='stable',
sandbox_compat=None,
last_modified=None):
"""Add a version with specified file types to the builder.
Args:
types: List of distribution types to add. Defaults to ['signed'].
last_modified: datetime or string. Defaults to a fixed time.
"""
if types is None:
types = ['signed']
if last_modified is None:
last_modified = '2026-02-10T12:00:00.000Z'
# Pre-populate to avoid queries.
builder.set_chromium_version(cef_version, chromium_version)
if sandbox_compat is not None:
builder.set_sandbox_compat(cef_version, sandbox_compat)
for t in types:
name = cef_json_builder.get_file_name(cef_version, platform, t,
channel) + '.tar.bz2'
builder.add_file(name, 12345, last_modified, 'a' * 40)
# --- test_get_stable_milestone ---
def test_get_stable_milestone(self):
builder = self._create_builder()
self._add_version(builder, 'linux64',
'136.0.3+g1111111+chromium-136.0.6778.0', '136.0.6778.0')
self._add_version(builder, 'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6', '137.0.7204.6')
ib = cef_installer_builder(builder)
self.assertEqual(137, ib.get_stable_milestone())
def test_get_stable_milestone_empty(self):
builder = self._create_builder()
ib = cef_installer_builder(builder)
self.assertIsNone(ib.get_stable_milestone())
# --- test_generate_stable_txt ---
def test_generate_stable_txt(self):
builder = self._create_builder()
self._add_version(builder, 'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6', '137.0.7204.6')
ib = cef_installer_builder(builder)
self.assertEqual('137', ib.generate_stable_txt())
def test_generate_stable_txt_empty(self):
builder = self._create_builder()
ib = cef_installer_builder(builder)
self.assertIsNone(ib.generate_stable_txt())
# --- test_generate_milestone_json ---
def test_generate_milestone_json_basic(self):
builder = self._create_builder()
# Add versions on two platforms (use different cef_versions to avoid
# sandbox_compat cache sharing across platforms).
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890')
self._add_version(builder, 'linux64',
'137.3.4+g1111111+chromium-137.0.7204.5', '137.0.7204.5')
ib = cef_installer_builder(builder)
result = json.loads(ib.generate_milestone_json(137))
# Both platforms present.
self.assertIn('windows64', result)
self.assertIn('linux64', result)
# Windows has abi_hash.
self.assertEqual('137.3.5', result['windows64']['version'])
self.assertEqual('abc123def4567890', result['windows64']['abi_hash'])
self.assertIn('signed', result['windows64']['file'])
# Linux has no abi_hash.
self.assertEqual('137.3.4', result['linux64']['version'])
self.assertNotIn('abi_hash', result['linux64'])
def test_generate_milestone_json_newest_version(self):
builder = self._create_builder()
# Older version added first with earlier timestamp.
self._add_version(builder,
'linux64',
'137.1.1+g1111111+chromium-137.0.7204.0',
'137.0.7204.0',
last_modified='2026-02-08T12:00:00.000Z')
# Newer version added second with later timestamp.
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
last_modified='2026-02-10T12:00:00.000Z')
ib = cef_installer_builder(builder)
result = json.loads(ib.generate_milestone_json(137))
# Should pick the newest version.
self.assertEqual('137.3.5', result['linux64']['version'])
def test_generate_milestone_json_no_abi_hash(self):
builder = self._create_builder()
self._add_version(builder, 'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6', '137.0.7204.6')
ib = cef_installer_builder(builder)
result = json.loads(ib.generate_milestone_json(137))
self.assertNotIn('abi_hash', result['linux64'])
def test_generate_milestone_json_omits_platforms_without_signed(self):
builder = self._create_builder()
# Only add 'minimal' type, no 'signed'.
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
types=['minimal'])
ib = cef_installer_builder(builder)
result = json.loads(ib.generate_milestone_json(137))
self.assertNotIn('linux64', result)
def test_generate_milestone_json_empty(self):
builder = self._create_builder()
ib = cef_installer_builder(builder)
self.assertEqual('{}', ib.generate_milestone_json(999))
# --- test_generate_milestone_platform_json ---
def test_generate_milestone_platform_json_ordering(self):
builder = self._create_builder()
self._add_version(builder,
'linux64',
'137.1.1+g1111111+chromium-137.0.7204.0',
'137.0.7204.0',
last_modified='2026-02-08T12:00:00.000Z')
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
last_modified='2026-02-10T12:00:00.000Z')
ib = cef_installer_builder(builder)
result = json.loads(ib.generate_milestone_platform_json(137, 'linux64'))
self.assertEqual(2, len(result))
# Newest first.
self.assertEqual('137.3.5', result[0]['version'])
self.assertEqual('137.1.1', result[1]['version'])
def test_generate_milestone_platform_json_filters_channel(self):
builder = self._create_builder()
# Stable version.
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
channel='stable')
# Beta version.
self._add_version(builder,
'linux64',
'137.4.0+g2222222+chromium-137.0.7204.8',
'137.0.7204.8',
channel='beta')
ib = cef_installer_builder(builder)
# Stable query should only return stable.
result_stable = json.loads(
ib.generate_milestone_platform_json(137, 'linux64', channel='stable'))
self.assertEqual(1, len(result_stable))
self.assertEqual('137.3.5', result_stable[0]['version'])
# Beta query should only return beta.
result_beta = json.loads(
ib.generate_milestone_platform_json(137, 'linux64', channel='beta'))
self.assertEqual(1, len(result_beta))
self.assertEqual('137.4.0', result_beta[0]['version'])
def test_generate_milestone_platform_json_empty(self):
builder = self._create_builder()
ib = cef_installer_builder(builder)
self.assertEqual('[]', ib.generate_milestone_platform_json(999, 'linux64'))
# --- test_generate_abi_hash_platform_json ---
def test_generate_abi_hash_platform_json_basic(self):
builder = self._create_builder()
# Same abi_hash across two milestones.
self._add_version(builder,
'windows64',
'136.0.3+g1111111+chromium-136.0.6778.0',
'136.0.6778.0',
sandbox_compat='abc123def4567890',
last_modified='2026-02-05T12:00:00.000Z')
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890',
last_modified='2026-02-10T12:00:00.000Z')
ib = cef_installer_builder(builder)
result = json.loads(
ib.generate_abi_hash_platform_json('abc123def4567890', 'windows64'))
# Both milestones present.
self.assertEqual(2, len(result))
def test_generate_abi_hash_platform_json_ordering(self):
builder = self._create_builder()
self._add_version(builder,
'windows64',
'136.0.3+g1111111+chromium-136.0.6778.0',
'136.0.6778.0',
sandbox_compat='abc123def4567890',
last_modified='2026-02-05T12:00:00.000Z')
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890',
last_modified='2026-02-10T12:00:00.000Z')
ib = cef_installer_builder(builder)
result = json.loads(
ib.generate_abi_hash_platform_json('abc123def4567890', 'windows64'))
# Newest first.
self.assertEqual('137.3.5', result[0]['version'])
self.assertEqual('136.0.3', result[1]['version'])
def test_generate_abi_hash_platform_json_excludes_other_hashes(self):
builder = self._create_builder()
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890',
last_modified='2026-02-10T12:00:00.000Z')
self._add_version(builder,
'windows64',
'138.0.1+g3333333+chromium-138.0.7204.0',
'138.0.7204.0',
sandbox_compat='def456abc7890123',
last_modified='2026-02-12T12:00:00.000Z')
ib = cef_installer_builder(builder)
result = json.loads(
ib.generate_abi_hash_platform_json('abc123def4567890', 'windows64'))
self.assertEqual(1, len(result))
self.assertEqual('137.3.5', result[0]['version'])
def test_generate_abi_hash_platform_json_non_windows(self):
builder = self._create_builder()
self._add_version(builder, 'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6', '137.0.7204.6')
ib = cef_installer_builder(builder)
self.assertEqual(
'[]', ib.generate_abi_hash_platform_json('abc123def4567890', 'linux64'))
self.assertEqual(
'[]', ib.generate_abi_hash_platform_json('abc123def4567890',
'macosx64'))
# --- test_get_abi_hashes ---
def test_get_abi_hashes(self):
builder = self._create_builder()
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890')
self._add_version(builder,
'windows64',
'138.0.1+g3333333+chromium-138.0.7204.0',
'138.0.7204.0',
sandbox_compat='def456abc7890123')
ib = cef_installer_builder(builder)
hashes = ib.get_abi_hashes(platform='windows64')
self.assertEqual({'abc123def4567890', 'def456abc7890123'}, hashes)
def test_get_abi_hashes_non_windows(self):
builder = self._create_builder()
self._add_version(builder, 'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6', '137.0.7204.6')
ib = cef_installer_builder(builder)
self.assertEqual(set(), ib.get_abi_hashes(platform='linux64'))
# --- test_get_milestones ---
def test_get_milestones(self):
builder = self._create_builder()
self._add_version(builder, 'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6', '137.0.7204.6')
self._add_version(builder, 'linux64',
'136.0.3+g1111111+chromium-136.0.6778.0', '136.0.6778.0')
ib = cef_installer_builder(builder)
self.assertEqual([136, 137], ib.get_milestones())
# --- test_skips_versions_without_signed_file ---
def test_skips_versions_without_signed_file(self):
builder = self._create_builder()
# Only add 'minimal' and 'client' types, no 'signed'.
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
types=['minimal', 'client'])
ib = cef_installer_builder(builder)
result = json.loads(ib.generate_milestone_platform_json(137, 'linux64'))
self.assertEqual(0, len(result))
# --- test_revoked_versions_excluded ---
def test_revoked_versions_excluded(self):
builder = self._create_builder()
self._add_version(builder,
'linux64',
'137.1.0+g1111111+chromium-137.0.7204.0',
'137.0.7204.0',
last_modified='2026-02-08T12:00:00.000Z')
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
last_modified='2026-02-10T12:00:00.000Z')
revoked = {
'revoked_versions': [{
'version': '137.1.0',
'reason': 'CVE-2024-XXXXX',
'revoked_at': '2024-01-15T00:00:00Z'
}]
}
ib = cef_installer_builder(builder, revoked_json=revoked)
# milestone_platform_json should exclude the revoked version.
result = json.loads(ib.generate_milestone_platform_json(137, 'linux64'))
self.assertEqual(1, len(result))
self.assertEqual('137.3.5', result[0]['version'])
# milestone_json should pick non-revoked newest.
result_milestone = json.loads(ib.generate_milestone_json(137))
self.assertEqual('137.3.5', result_milestone['linux64']['version'])
def test_revoked_versions_excluded_from_abi_hash(self):
builder = self._create_builder()
self._add_version(builder,
'windows64',
'137.1.0+g1111111+chromium-137.0.7204.0',
'137.0.7204.0',
sandbox_compat='abc123def4567890',
last_modified='2026-02-08T12:00:00.000Z')
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890',
last_modified='2026-02-10T12:00:00.000Z')
revoked = {
'revoked_versions': [{
'version': '137.1.0',
'reason': 'test',
'revoked_at': '2024-01-15T00:00:00Z'
}]
}
ib = cef_installer_builder(builder, revoked_json=revoked)
result = json.loads(
ib.generate_abi_hash_platform_json('abc123def4567890', 'windows64'))
self.assertEqual(1, len(result))
self.assertEqual('137.3.5', result[0]['version'])
def test_revoked_range_excludes_matching_versions(self):
builder = self._create_builder()
self._add_version(builder,
'linux64',
'137.1.0+g1111111+chromium-137.0.7204.0',
'137.0.7204.0',
last_modified='2026-02-08T12:00:00.000Z')
self._add_version(builder,
'linux64',
'137.2.0+g2222222+chromium-137.0.7204.2',
'137.0.7204.2',
last_modified='2026-02-09T12:00:00.000Z')
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
last_modified='2026-02-10T12:00:00.000Z')
revoked = {
'revoked_versions': [{
'version_min': '137.1.0',
'version_max': '137.2.99',
'reason': 'CVE-2024-YYYYY',
'revoked_at': '2024-02-01T00:00:00Z'
}]
}
ib = cef_installer_builder(builder, revoked_json=revoked)
result = json.loads(ib.generate_milestone_platform_json(137, 'linux64'))
self.assertEqual(1, len(result))
self.assertEqual('137.3.5', result[0]['version'])
def test_revoked_range_and_point_combined(self):
builder = self._create_builder()
self._add_version(builder,
'linux64',
'137.1.0+g1111111+chromium-137.0.7204.0',
'137.0.7204.0',
last_modified='2026-02-08T12:00:00.000Z')
self._add_version(builder,
'linux64',
'137.2.0+g2222222+chromium-137.0.7204.2',
'137.0.7204.2',
last_modified='2026-02-09T12:00:00.000Z')
self._add_version(builder,
'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
last_modified='2026-02-10T12:00:00.000Z')
revoked = {
'revoked_versions': [
{
'version': '137.3.5',
'reason': 'point revocation',
'revoked_at': '2024-01-15T00:00:00Z'
},
{
'version_min': '137.1.0',
'version_max': '137.1.99',
'reason': 'range revocation',
'revoked_at': '2024-02-01T00:00:00Z'
},
]
}
ib = cef_installer_builder(builder, revoked_json=revoked)
# Only 137.2.0 survives.
result = json.loads(ib.generate_milestone_platform_json(137, 'linux64'))
self.assertEqual(1, len(result))
self.assertEqual('137.2.0', result[0]['version'])
def test_revoked_range_partial_version_specs(self):
builder = self._create_builder()
self._add_version(builder,
'linux64',
'137.1.0+g1111111+chromium-137.0.7204.0',
'137.0.7204.0',
last_modified='2026-02-08T12:00:00.000Z')
self._add_version(builder,
'linux64',
'138.0.0+g2222222+chromium-138.0.7300.0',
'138.0.7300.0',
last_modified='2026-02-09T12:00:00.000Z')
self._add_version(builder,
'linux64',
'138.3.0+g3333333+chromium-138.0.7300.3',
'138.0.7300.3',
last_modified='2026-02-10T12:00:00.000Z')
self._add_version(builder,
'linux64',
'139.0.0+g4444444+chromium-139.0.7400.0',
'139.0.7400.0',
last_modified='2026-02-11T12:00:00.000Z')
# "138" means 138.0.0, "138.2" means 138.2.0 — revokes 138.0.0 but
# not 138.3.0.
revoked = {
'revoked_versions': [{
'version_min': '138',
'version_max': '138.2',
'reason': 'partial version spec test',
'revoked_at': '2024-02-01T00:00:00Z'
}]
}
ib = cef_installer_builder(builder, revoked_json=revoked)
result = json.loads(ib.generate_milestone_platform_json(138, 'linux64'))
self.assertEqual(1, len(result))
self.assertEqual('138.3.0', result[0]['version'])
# --- Round-trip tests ---
def test_round_trip_milestone_json(self):
builder = self._create_builder()
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890')
ib = cef_installer_builder(builder)
output = ib.generate_milestone_json(137)
parsed = json.loads(output)
self.assertIsInstance(parsed, dict)
self.assertIn('windows64', parsed)
def test_round_trip_milestone_platform_json(self):
builder = self._create_builder()
self._add_version(builder, 'linux64',
'137.3.5+g62d140e+chromium-137.0.7204.6', '137.0.7204.6')
ib = cef_installer_builder(builder)
output = ib.generate_milestone_platform_json(137, 'linux64')
parsed = json.loads(output)
self.assertIsInstance(parsed, list)
self.assertEqual(1, len(parsed))
def test_round_trip_abi_hash_platform_json(self):
builder = self._create_builder()
self._add_version(builder,
'windows64',
'137.3.5+g62d140e+chromium-137.0.7204.6',
'137.0.7204.6',
sandbox_compat='abc123def4567890')
ib = cef_installer_builder(builder)
output = ib.generate_abi_hash_platform_json('abc123def4567890', 'windows64')
parsed = json.loads(output)
self.assertIsInstance(parsed, list)
self.assertEqual(1, len(parsed))
class TestVersionTupleComparison(unittest.TestCase):
"""Verify _parse_version_tuple comparison matches base::Version semantics.
base::Version treats missing trailing components as zero, so
Version("137") == Version("137.0") == Version("137.0.0"). The Python
tuple comparison must agree.
"""
_parse = staticmethod(cef_installer_builder._parse_version_tuple)
def test_single_component_equals_padded(self):
self.assertEqual(self._parse('137'), self._parse('137.0.0'))
def test_two_components_equals_padded(self):
self.assertEqual(self._parse('137.4'), self._parse('137.4.0'))
def test_three_components_unchanged(self):
self.assertEqual(self._parse('137.3.5'), (137, 3, 5))
def test_less_than_across_component_counts(self):
self.assertLess(self._parse('137'), self._parse('137.0.1'))
self.assertLess(self._parse('137.3'), self._parse('137.3.1'))
self.assertLess(self._parse('136'), self._parse('137'))
def test_greater_than_across_component_counts(self):
self.assertGreater(self._parse('138'), self._parse('137.99.99'))
self.assertGreater(self._parse('137.4'), self._parse('137.3.99'))
def test_range_check_with_partial_bounds(self):
"""Simulate the range check: version_min <= version <= version_max."""
v = self._parse('137.3.5')
self.assertTrue(self._parse('137') <= v <= self._parse('137.99'))
self.assertTrue(self._parse('137.3') <= v <= self._parse('137.3.5'))
self.assertFalse(self._parse('137.3') <= v <= self._parse('137.3'))
self.assertFalse(self._parse('137.4') <= v <= self._parse('138'))
self.assertFalse(self._parse('137') <= v <= self._parse('137.3.4'))
# Program entry point.
if __name__ == '__main__':
unittest.main()
+29 -7
View File
@@ -90,6 +90,10 @@ _cef_version_dev_regex = _cef_number_regex + r'\-\w+\.[0-9]{1,7}\+' + _cef_hash_
class cef_json_builder:
""" Class used to build the cefbuilds JSON file. """
# Minimum milestone that supports sandbox_compat (ABI hash) queries.
# Can be overridden for testing.
MIN_SANDBOX_COMPAT_MILESTONE = 146
def __init__(self, prettyprint=False, silent=True):
""" Create a new cef_json_builder object. """
self._prettyprint = prettyprint
@@ -107,7 +111,7 @@ class cef_json_builder:
def get_distrib_types():
""" Returns the list of supported distribution types. """
return ('standard', 'minimal', 'client', 'tools', 'release_symbols',
'debug_symbols')
'debug_symbols', 'signed')
@staticmethod
def is_valid_version(version):
@@ -134,6 +138,16 @@ class cef_json_builder:
# 16-character hex string
return bool(re.compile('^[0-9a-f]{16}$').match(sandbox_compat))
@staticmethod
def get_short_version(cef_version):
"""Extract short version (e.g., '137.3.5') from full CEF version."""
return cef_version.split('+')[0]
@staticmethod
def get_milestone(cef_version):
"""Extract milestone (major version number) from CEF version."""
return int(cef_version.split('.')[0])
@staticmethod
def cef_version_to_api_version(cef_version):
""" Convert CEF version to API version number.
@@ -190,12 +204,11 @@ class cef_json_builder:
# Return a string representation of this object.
self._sort_versions()
if self._prettyprint:
return json.dumps(
self._data,
cls=cef_json_encoder,
sort_keys=True,
indent=2,
separators=(',', ': '))
return json.dumps(self._data,
cls=cef_json_encoder,
sort_keys=True,
indent=2,
separators=(',', ': '))
else:
return json.dumps(self._data, cls=cef_json_encoder, sort_keys=True)
@@ -312,6 +325,10 @@ class cef_json_builder:
# Old-style version numbers don't have API versioning.
return None
# sandbox_compat (ABI hash) only exists for recent milestones.
if self.get_milestone(cef_version) < self.MIN_SANDBOX_COMPAT_MILESTONE:
return None
json_data = self._query_api_versions_json(cef_version)
if json_data is None or 'hashes' not in json_data:
return None
@@ -494,6 +511,11 @@ class cef_json_builder:
type = 'tools'
del name_parts[-1]
# Might be '<version>_<platform>_signed'.
if name_parts[-1] == 'signed':
type = 'signed'
del name_parts[-1]
# Might be '<version>_<platform>_beta'.
if name_parts[-1] == 'beta':
del name_parts[-1]
+81 -4
View File
@@ -11,12 +11,17 @@
# Add files for all platforms at the specified version:
# > python cef_json_builder_example.py add all 3.2704.1416.g185cd6c 51.0.2704.47
#
# Add files and also generate installer query files:
# > python cef_json_builder_example.py add all 137.3.5+g62d140e+chromium-137.0.7204.6 137.0.7204.6 --installer
#
# See cef_json_builder.get_platforms() for the list of supported platforms.
from __future__ import absolute_import
from __future__ import print_function
from cef_json_builder import cef_json_builder
from cef_installer_builder import cef_installer_builder
import datetime
import json
import os
import random
import string
@@ -25,8 +30,8 @@ import sys
# Create a fake sha1 checksum value.
def make_fake_sha1():
return ''.join(random.SystemRandom().choice('abcdef' + string.digits)
for _ in range(40))
return ''.join(
random.SystemRandom().choice('abcdef' + string.digits) for _ in range(40))
# Create a fake file size value.
@@ -63,6 +68,9 @@ def create_fake_files(platform, version):
files.append(make_fake_file_info(platform, version, 'debug_symbols'))
files.append(make_fake_file_info(platform, version, 'release_symbols'))
# All platforms create signed distributions (for installer use).
files.append(make_fake_file_info(platform, version, 'signed'))
return files
@@ -71,10 +79,13 @@ if __name__ == '__main__':
# Verify command-line arguments.
if len(sys.argv) < 5 or sys.argv[1] != 'add':
sys.stderr.write(
'Usage: %s add <platform> <cef_version> <chromium_version>\n' %
sys.argv[0])
'Usage: %s add <platform> <cef_version> <chromium_version> [--installer]\n'
% sys.argv[0])
sys.exit()
# Check for --installer flag.
generate_installer = '--installer' in sys.argv
# Requested platform.
if sys.argv[2] == 'all':
platforms = cef_json_builder.get_platforms()
@@ -148,3 +159,69 @@ if __name__ == '__main__':
# A real implementation would now upload the changed files.
for file in changed_files:
print('--> Upload file %s' % file['name'])
# Optionally generate installer query files.
if generate_installer:
# Load revoked.json if it exists.
revoked_file = os.path.join(current_dir, 'revoked.json')
revoked_json = None
if os.path.exists(revoked_file):
print('--> Reading revoked.json')
with open(revoked_file, 'r') as f:
revoked_json = json.loads(f.read())
installer = cef_installer_builder(builder, revoked_json=revoked_json)
# Generate stable.txt.
for channel in ('stable', 'beta'):
stable_txt = installer.generate_stable_txt(channel=channel)
if stable_txt is not None:
filename = '%s.txt' % channel
filepath = os.path.join(current_dir, filename)
print('--> Writing %s' % filename)
with open(filepath, 'w') as f:
f.write(stable_txt)
# Generate per-milestone and per-milestone+platform JSON files.
for channel in ('stable', 'beta'):
for milestone in installer.get_milestones(channel=channel):
channel_suffix = '_' + channel if channel != 'stable' else ''
# {milestone}.json
filename = '%d%s.json' % (milestone, channel_suffix)
filepath = os.path.join(current_dir, filename)
print('--> Writing %s' % filename)
with open(filepath, 'w') as f:
f.write(
installer.generate_milestone_json(milestone,
channel=channel,
prettyprint=True))
# {milestone}_{platform}.json
for platform in cef_json_builder.get_platforms():
content = installer.generate_milestone_platform_json(milestone,
platform,
channel=channel,
prettyprint=True)
if content != '[]':
filename = '%d_%s%s.json' % (milestone, platform, channel_suffix)
filepath = os.path.join(current_dir, filename)
print('--> Writing %s' % filename)
with open(filepath, 'w') as f:
f.write(content)
# Generate per-abi_hash+platform JSON files (Windows only).
for channel in ('stable', 'beta'):
for abi_hash in installer.get_abi_hashes(channel=channel):
channel_suffix = '_' + channel if channel != 'stable' else ''
for platform in ('windows32', 'windows64', 'windowsarm64'):
content = installer.generate_abi_hash_platform_json(abi_hash,
platform,
channel=channel,
prettyprint=True)
if content != '[]':
filename = '%s_%s%s.json' % (abi_hash, platform, channel_suffix)
filepath = os.path.join(current_dir, filename)
print('--> Writing %s' % filename)
with open(filepath, 'w') as f:
f.write(content)
+104 -49
View File
@@ -12,6 +12,14 @@ from unittest.mock import patch, MagicMock
class TestCefJSONBuilder(unittest.TestCase):
def setUp(self):
# Allow sandbox_compat queries for all milestones in tests.
self._orig_min = cef_json_builder.MIN_SANDBOX_COMPAT_MILESTONE
cef_json_builder.MIN_SANDBOX_COMPAT_MILESTONE = 0
def tearDown(self):
cef_json_builder.MIN_SANDBOX_COMPAT_MILESTONE = self._orig_min
# Test CEF version number matching.
def test_valid_version(self):
# Old-style version numbers.
@@ -212,8 +220,10 @@ class TestCefJSONBuilder(unittest.TestCase):
builder = cef_json_builder()
# Specify all values just in case the defaults change.
expected = self._add_test_file(
builder, platform='linux32', version=version, type='standard')
expected = self._add_test_file(builder,
platform='linux32',
version=version,
type='standard')
# No filter.
files = builder.get_files()
@@ -247,18 +257,22 @@ class TestCefJSONBuilder(unittest.TestCase):
self.assertEqual(len(files), 0)
# All filters.
files = builder.get_files(
platform='linux32', version=partial_version, type='standard')
files = builder.get_files(platform='linux32',
version=partial_version,
type='standard')
self.assertEqual(len(files), 1)
self.assertEqual(expected, files[0])
files = builder.get_files(
platform='linux32', version=partial_version, type='minimal')
files = builder.get_files(platform='linux32',
version=partial_version,
type='minimal')
self.assertEqual(len(files), 0)
files = builder.get_files(
platform='linux32', version='3.2623', type='standard')
files = builder.get_files(platform='linux32',
version='3.2623',
type='standard')
self.assertEqual(len(files), 0)
files = builder.get_files(
platform='linux64', version=partial_version, type='standard')
files = builder.get_files(platform='linux64',
version=partial_version,
type='standard')
self.assertEqual(len(files), 0)
# Test add/get of multiple files.
@@ -280,8 +294,10 @@ class TestCefJSONBuilder(unittest.TestCase):
for version in versions:
for type in types:
expected.append(
self._add_test_file(
builder, platform=platform, type=type, version=version))
self._add_test_file(builder,
platform=platform,
type=type,
version=version))
self._verify_write_read(builder)
@@ -307,8 +323,9 @@ class TestCefJSONBuilder(unittest.TestCase):
for platform in platforms:
for version in versions:
for type in types:
files = builder.get_files(
platform=platform, type=type, version=version)
files = builder.get_files(platform=platform,
type=type,
version=version)
self.assertEqual(len(files), 1)
self.assertEqual(expected[idx], files[0])
idx += 1
@@ -325,8 +342,10 @@ class TestCefJSONBuilder(unittest.TestCase):
# Initial file versions.
for platform in platforms:
for type in types:
self._add_test_file(
builder, platform=platform, type=type, version=version)
self._add_test_file(builder,
platform=platform,
type=type,
version=version)
# No filter.
files = builder.get_files()
@@ -338,12 +357,11 @@ class TestCefJSONBuilder(unittest.TestCase):
for platform in platforms:
for type in types:
expected.append(
self._add_test_file(
builder,
platform=platform,
type=type,
version=version,
attrib_idx=1))
self._add_test_file(builder,
platform=platform,
type=type,
version=version,
attrib_idx=1))
# No filter.
files = builder.get_files()
@@ -353,8 +371,9 @@ class TestCefJSONBuilder(unittest.TestCase):
idx = 0
for platform in platforms:
for type in types:
files = builder.get_files(
platform=platform, type=type, version=version)
files = builder.get_files(platform=platform,
type=type,
version=version)
self.assertEqual(len(files), 1)
self.assertEqual(expected[idx], files[0])
idx += 1
@@ -371,8 +390,10 @@ class TestCefJSONBuilder(unittest.TestCase):
# Initial file versions.
for platform in platforms:
for type in types:
self._add_test_file(
builder, platform=platform, type=type, version=version)
self._add_test_file(builder,
platform=platform,
type=type,
version=version)
# No filter.
files = builder.get_files()
@@ -384,12 +405,11 @@ class TestCefJSONBuilder(unittest.TestCase):
for platform in platforms:
for type in types:
expected.append(
self._add_test_file(
builder,
platform=platform,
type=type,
version=version,
shouldfail=True))
self._add_test_file(builder,
platform=platform,
type=type,
version=version,
shouldfail=True))
# No filter.
files = builder.get_files()
@@ -399,8 +419,9 @@ class TestCefJSONBuilder(unittest.TestCase):
idx = 0
for platform in platforms:
for type in types:
files = builder.get_files(
platform=platform, type=type, version=version)
files = builder.get_files(platform=platform,
type=type,
version=version)
self.assertEqual(len(files), 1)
self.assertEqual(expected[idx], files[0])
idx += 1
@@ -495,21 +516,26 @@ class TestCefJSONBuilder(unittest.TestCase):
# Test CEF version to API version conversion.
def test_cef_version_to_api_version(self):
# New-style version numbers.
self.assertEqual('13800',
cef_json_builder.cef_version_to_api_version(
'138.0.1+g62d140e+chromium-138.0.7204.0'))
self.assertEqual('13801',
cef_json_builder.cef_version_to_api_version(
'138.1.0+g62d140e+chromium-138.0.7204.0'))
self.assertEqual('13802',
cef_json_builder.cef_version_to_api_version(
'138.2.5+g62d140e+chromium-138.0.7204.0'))
self.assertEqual('7401',
cef_json_builder.cef_version_to_api_version(
'74.1.0+g62d140e+chromium-74.0.3729.6'))
self.assertEqual('10000',
cef_json_builder.cef_version_to_api_version(
'100.0.0+g62d140e+chromium-100.0.4896.0'))
self.assertEqual(
'13800',
cef_json_builder.cef_version_to_api_version(
'138.0.1+g62d140e+chromium-138.0.7204.0'))
self.assertEqual(
'13801',
cef_json_builder.cef_version_to_api_version(
'138.1.0+g62d140e+chromium-138.0.7204.0'))
self.assertEqual(
'13802',
cef_json_builder.cef_version_to_api_version(
'138.2.5+g62d140e+chromium-138.0.7204.0'))
self.assertEqual(
'7401',
cef_json_builder.cef_version_to_api_version(
'74.1.0+g62d140e+chromium-74.0.3729.6'))
self.assertEqual(
'10000',
cef_json_builder.cef_version_to_api_version(
'100.0.0+g62d140e+chromium-100.0.4896.0'))
# Old-style version numbers return None (no API versioning).
self.assertIsNone(
@@ -714,6 +740,35 @@ class TestCefJSONBuilder(unittest.TestCase):
self.assertEqual(1, len(versions))
self.assertEqual('abc123def4567890', versions[0].get('sandbox_compat'))
# Test get_short_version static method.
def test_get_short_version(self):
# New-style version returns short version.
self.assertEqual(
'137.3.5',
cef_json_builder.get_short_version(
'137.3.5+g62d140e+chromium-137.0.7204.6'))
# Old-style version returns full string (no '+').
self.assertEqual('3.2704.1414.g185cd6c',
cef_json_builder.get_short_version('3.2704.1414.g185cd6c'))
# Test get_milestone static method.
def test_get_milestone(self):
# New-style version returns milestone.
self.assertEqual(
137,
cef_json_builder.get_milestone(
'137.3.5+g62d140e+chromium-137.0.7204.6'))
# Old-style version returns 3.
self.assertEqual(3, cef_json_builder.get_milestone('3.2704.1414.g185cd6c'))
# Test add/get of a signed type file.
def test_add_signed_file(self):
self._test_add_file('signed')
# Test add/get of a signed type file in beta channel.
def test_add_signed_file_beta(self):
self._test_add_file('signed', channel='beta')
# Test set_sandbox_compat with explicit value.
def test_set_sandbox_compat_explicit(self):
builder = cef_json_builder(silent=True)
+3
View File
@@ -0,0 +1,3 @@
{
"revoked_versions": []
}