From 7324f016a35938246dca9841e56982ef538c51b6 Mon Sep 17 00:00:00 2001 From: Marshall Greenblatt Date: Fri, 13 Mar 2026 16:28:04 -0400 Subject: [PATCH] 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. --- tools/cefbuilds/README.md | 142 ++++ tools/cefbuilds/cef_installer_builder.py | 235 +++++++ tools/cefbuilds/cef_installer_builder_test.py | 618 ++++++++++++++++++ tools/cefbuilds/cef_json_builder.py | 36 +- tools/cefbuilds/cef_json_builder_example.py | 85 ++- tools/cefbuilds/cef_json_builder_test.py | 153 +++-- tools/cefbuilds/revoked.json | 3 + 7 files changed, 1212 insertions(+), 60 deletions(-) create mode 100644 tools/cefbuilds/README.md create mode 100644 tools/cefbuilds/cef_installer_builder.py create mode 100644 tools/cefbuilds/cef_installer_builder_test.py create mode 100644 tools/cefbuilds/revoked.json diff --git a/tools/cefbuilds/README.md b/tools/cefbuilds/README.md new file mode 100644 index 000000000..497a17cff --- /dev/null +++ b/tools/cefbuilds/README.md @@ -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 +``` diff --git a/tools/cefbuilds/cef_installer_builder.py b/tools/cefbuilds/cef_installer_builder.py new file mode 100644 index 000000000..e9c61a00f --- /dev/null +++ b/tools/cefbuilds/cef_installer_builder.py @@ -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) diff --git a/tools/cefbuilds/cef_installer_builder_test.py b/tools/cefbuilds/cef_installer_builder_test.py new file mode 100644 index 000000000..f4feafedf --- /dev/null +++ b/tools/cefbuilds/cef_installer_builder_test.py @@ -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() diff --git a/tools/cefbuilds/cef_json_builder.py b/tools/cefbuilds/cef_json_builder.py index c55ff76d7..8ec6b6efc 100644 --- a/tools/cefbuilds/cef_json_builder.py +++ b/tools/cefbuilds/cef_json_builder.py @@ -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 '__signed'. + if name_parts[-1] == 'signed': + type = 'signed' + del name_parts[-1] + # Might be '__beta'. if name_parts[-1] == 'beta': del name_parts[-1] diff --git a/tools/cefbuilds/cef_json_builder_example.py b/tools/cefbuilds/cef_json_builder_example.py index f9468b1af..ba125ebb5 100644 --- a/tools/cefbuilds/cef_json_builder_example.py +++ b/tools/cefbuilds/cef_json_builder_example.py @@ -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 \n' % - sys.argv[0]) + 'Usage: %s add [--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) diff --git a/tools/cefbuilds/cef_json_builder_test.py b/tools/cefbuilds/cef_json_builder_test.py index afb1630ea..3677cf897 100644 --- a/tools/cefbuilds/cef_json_builder_test.py +++ b/tools/cefbuilds/cef_json_builder_test.py @@ -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) diff --git a/tools/cefbuilds/revoked.json b/tools/cefbuilds/revoked.json new file mode 100644 index 000000000..1f0f09890 --- /dev/null +++ b/tools/cefbuilds/revoked.json @@ -0,0 +1,3 @@ +{ + "revoked_versions": [] +}