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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions .github/workflows/generated-bindings-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Validate and resolve tested compatibility
id: compat
shell: bash
working-directory: ${{ github.workspace }}
run: |
python ./scripts/test_compatibility.py
python ./scripts/test_doctor.py
python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT"
- name: Accept only generated binding files
env:
BASE_REF: ${{ github.base_ref }}
Expand Down Expand Up @@ -78,35 +86,38 @@ jobs:
- os: ubuntu-24.04
compiler: gcc
v-channel: stable
v-version: 0.5.2
- os: ubuntu-24.04
compiler: gcc
v-channel: master
v-version: master
- os: ubuntu-24.04
compiler: tcc
v-channel: v3
v-version: efae23e85b3772a28e62884d554c1e88f91f52f4
- os: macos-14
compiler: clang
v-channel: stable
v-version: 0.5.2
- os: windows-2022
compiler: msvc
v-channel: stable
v-version: 0.5.2
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Validate and resolve tested compatibility
id: compat
shell: bash
working-directory: ${{ github.workspace }}
run: |
python ./scripts/test_compatibility.py
python ./scripts/test_doctor.py
python ./scripts/compatibility.py --profile '${{ matrix.v-channel }}' --github-output "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: antono2/v_vulkan_bindings
ref: acacf0b8043b10364549382d02535bd5d90309a1
ref: ${{ steps.compat.outputs.generator }}
path: generator
- uses: prantlf/setup-v-action@92f20bea50d2c9931a28c96bb4997a3c7357baf0 # v4
if: matrix.v-channel == 'stable'
with:
version: ${{ matrix.v-version }}
version: ${{ steps.compat.outputs.selected_v }}
# Source-built V revisions include the v1_fallback executable needed by
# tool commands such as `v test`; setup-v-action@v4 omits it from cache.
- name: Check out source-built V
Expand All @@ -116,23 +127,23 @@ jobs:
continue-on-error: ${{ matrix.v-channel == 'master' }}
with:
repository: vlang/v
ref: ${{ matrix.v-version }}
ref: ${{ steps.compat.outputs.selected_v }}
path: .ci-v-source
fetch-depth: 1
- name: Check out the matching v3 bootstrap source
if: matrix.v-channel == 'v3' && steps.checkout_v_source.outcome == 'success'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: vlang/vc
ref: 2cbaf6bc2367937cd5f0b4ef923d1ef9aad613b3
ref: ${{ steps.compat.outputs.v3_vc }}
path: .ci-v-source/vc
fetch-depth: 1
- name: Check out the matching v3 TinyCC runtime
if: matrix.v-channel == 'v3' && steps.checkout_v_source.outcome == 'success'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: vlang/tccbin
ref: d6e7ac1b1bcc98aed734a6ecbfa8509f24606c74
ref: ${{ steps.compat.outputs.v3_tcc }}
path: .ci-v-source/thirdparty/tcc
fetch-depth: 1
- name: Build source V compiler
Expand All @@ -151,7 +162,7 @@ jobs:
- name: Install Vulkan SDK and SwiftShader test artifacts
if: matrix.v-channel == 'stable' || steps.setup_v_source.outcome == 'success'
shell: bash
run: bash .github/scripts/setup-vulkan-ci.sh 1.4.309.0 v1.2
run: bash .github/scripts/setup-vulkan-ci.sh ${{ steps.compat.outputs.ci_sdk }} ${{ steps.compat.outputs.ci_runtime }}
- name: Verify bundled headers and Volk against pinned upstream sources
if: matrix.v-channel == 'stable' || steps.setup_v_source.outcome == 'success'
shell: bash
Expand Down Expand Up @@ -335,6 +346,14 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Validate and resolve tested compatibility
id: compat
shell: bash
working-directory: ${{ github.workspace }}
run: |
python ./scripts/test_compatibility.py
python ./scripts/test_doctor.py
python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT"
- name: Validate and publish the package release
env:
GH_TOKEN: ${{ github.token }}
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,10 @@ where they explain compatibility.

## Thanks

- [Delyan Angelov (@spytheman)](https://github.com/spytheman) — for fixing my bugs in the early days.
- [Jalon Solov (@JalonSolov)](https://github.com/JalonSolov) — for quick and solid answers to obscure questions.
- [Alexander Medvednikov (@medvednikov)](https://github.com/medvednikov) — for creating V and relentless productivity.
- The awesome team at [V](https://vlang.io/) — for making this possible.
- [Delyan Angelov (@spytheman)](https://github.com/spytheman) — for fixing my bugs in the early days.
- [Jalon Solov (@JalonSolov)](https://github.com/JalonSolov) — for quick and solid answers to obscure questions.
- [Alexander Medvednikov (@medvednikov)](https://github.com/medvednikov) — for creating V and relentless productivity.
- The awesome team at [V](https://vlang.io/) — for making this possible.

## Source navigation

Expand All @@ -261,3 +261,9 @@ verified unchanged. Select Vulkan-Docs using this module's `VERSION`; the
generator repository's historical default snapshot may be older. Despite its
name, `REGISTRY_COMMIT` pins **Vulkan-Headers**, not Vulkan-Docs. Keep that C-header
pin and `VOLK_COMMIT` aligned with the inputs described in [the vendor guide](c/vendor/README.md).


## Compatibility and diagnostics

See [tested profiles and read-only diagnostics](docs/compatibility.md) for
compiler/dependency versions, setup recovery actions and upgrade checks.
15 changes: 15 additions & 0 deletions compatibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"schema_version": 1,
"v_release": "0.5.2",
"v3_v": "efae23e85b3772a28e62884d554c1e88f91f52f4",
"v3_vc": "2cbaf6bc2367937cd5f0b4ef923d1ef9aad613b3",
"v3_tcc": "d6e7ac1b1bcc98aed734a6ecbfa8509f24606c74",
"ci_sdk": "1.4.309.0",
"ci_runtime": "v1.2",
"generator": "acacf0b8043b10364549382d02535bd5d90309a1",
"modules": {},
"build_tools": [
"git",
"cmake"
]
}
23 changes: 23 additions & 0 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Tested compatibility and diagnostics

`compatibility.json` records the existing tested compiler, bootstrap, native and
module versions. CI resolves these values before dependency checkout and rejects
disagreement with `v.mod`. Advisory upstream-master lanes remain advisory.
Registry/version files owned by binding generators remain authoritative for
generated API contents; upstream update workflows deliberately select candidates
separately from the tested compiler profiles.

Upgrade the manifest and affected VPM declarations together. Run
`python scripts/test_compatibility.py` and the full project CI. Consumer application
compilation/runtime checks are also required before publishing a dependency upgrade.
The resolver's `--modules PATH` option verifies installed Git tags without fetching.

Run `python scripts/doctor.py --v PATH_TO_V --modules PATH_TO_MODULES` for read-only
compiler/tool/dependency diagnostics. Add `--json` for a support report and
`--runtime` for Vulkan device enumeration when Vulkan tools are available. Missing
or mismatched prerequisites include a recovery action; diagnostics never repair
caches or change the environment. Enumeration alone does not prove hardware video
decoding or OpenCL interoperability. Python 3.9 or newer is required for these tools.

Choose `--profile v3` for the pinned V3 compiler or `--profile release` for
the released compiler when the project defines multiple profiles.
65 changes: 65 additions & 0 deletions scripts/compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Resolve tested compiler/native/module profiles and validate VPM declarations."""
import argparse
import json
from pathlib import Path
import re
import subprocess


def resolve(root, profile='stable'):
pins = json.loads((root / 'compatibility.json').read_text())
if pins.get('schema_version') != 1:
raise ValueError('Unsupported compatibility schema_version')
if 'v_release' in pins and not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', pins['v_release']):
raise ValueError('v_release must be a released compiler version')
upstream = root / 'third_party/upstream.json'
if upstream.is_file() and pins.get('glfw') != json.loads(upstream.read_text())['version']:
raise ValueError('Native GLFW version and compatibility.json disagree')
for key, value in pins.items():
if key in ['v', 'vc', 'tcc', 'volk', 'generator'] or key.endswith(('_v', '_vc', '_tcc')):
if not re.fullmatch('[0-9a-f]{40}', value):
raise ValueError(f'{key} must be an immutable Git commit')
declared = dict(re.findall(r"'antono2\.([a-z0-9_]+)@([^']+)'", (root / 'v.mod').read_text()))
for name, tag in declared.items():
if pins.get('modules', {}).get(name) != tag:
raise ValueError(f'v.mod and compatibility.json disagree for {name}')
outputs = {key: value for key, value in pins.items() if isinstance(value, str)}
for name, tag in pins.get('modules', {}).items():
if not re.fullmatch('[a-z0-9_]+', name) or not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+(?:-standard)?', tag):
raise ValueError(f'Invalid module pin: {name}={tag}')
outputs['module_' + name] = tag
outputs['selected_v'] = 'master' if profile == 'master' else (pins['v3_v'] if profile == 'v3' else pins.get('v', pins.get('v_release', '')))
for key, value in outputs.items():
if not re.fullmatch('[a-z0-9_]+', key) or '\n' in value or '\r' in value:
raise ValueError(f'Unsafe compatibility output: {key}')
return outputs


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument('--profile', choices=['stable', 'v3', 'master'], default='stable')
parser.add_argument('--github-output', type=Path)
parser.add_argument('--get')
parser.add_argument('--modules', type=Path)
args = parser.parse_args()
outputs = resolve(args.root, args.profile)
if args.modules:
for key, tag in outputs.items():
if not key.startswith('module_'):
continue
checkout = args.modules / 'antono2' / key[len('module_'):]
def revision(ref):
return subprocess.check_output(['git', '-C', str(checkout), 'rev-parse', '--verify', ref], text=True).strip()
if revision('HEAD') != revision(f'refs/tags/{tag}^{{commit}}'):
raise ValueError(f'Installed module does not match {tag}: {checkout}')
if args.github_output:
with args.github_output.open('a', encoding='utf-8') as output:
for key, value in outputs.items():
output.write(f'{key}={value}\n')
print(outputs[args.get] if args.get else json.dumps(outputs, indent=2))


if __name__ == '__main__':
main()
97 changes: 97 additions & 0 deletions scripts/doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Read-only build/runtime diagnostics; use --json for a support report."""
import argparse
import json
import os
from pathlib import Path
import re
import shutil
import subprocess


def probe(command, timeout=15):
try:
result = subprocess.run(command, capture_output=True, text=True, errors='replace', timeout=timeout)
return result.returncode == 0, (result.stdout + result.stderr).strip()
except (OSError, subprocess.TimeoutExpired) as error:
return False, str(error)


def diagnose(root, compiler=None, modules=None, sdk=None, runtime=False, profile='stable'):
checks = []
def check(name, passed, detail, action, optional=False):
checks.append({'name': name, 'status': 'pass' if passed else ('warn' if optional else 'fail'),
'detail': detail, 'action': '' if passed else action})
try:
pins = json.loads((root / 'compatibility.json').read_text())
if pins.get('schema_version') != 1:
raise ValueError('Unsupported schema_version')
except (OSError, ValueError) as error:
check('Compatibility manifest', False, str(error), 'Restore compatibility.json from this project revision.')
return {'checks': checks}
declared = dict(re.findall(r"'antono2\.([a-z0-9_]+)@([^']+)'", (root / 'v.mod').read_text()))
disagreement = [name for name, tag in declared.items() if pins.get('modules', {}).get(name) != tag]
check('Declared dependencies', not disagreement, ', '.join(disagreement) or 'Manifest agrees with v.mod',
'Update compatibility.json and v.mod together; use the tested dependency versions.')
for tool in pins.get('build_tools', ['git', 'cmake']):
location = shutil.which(tool)
check(tool, bool(location), location or 'Not found on PATH', f'Install {tool}, reopen your shell and rerun diagnostics.')
compiler = str(compiler) if compiler else (os.environ.get('VEXE') or shutil.which('v'))
if compiler:
passed, detail = probe([compiler, '-version'])
selected = pins.get('v_release', '') if profile == 'release' else (
pins.get('v3_v', pins.get('v', '')) if profile == 'v3' else pins.get('v', ''))
expected = selected[:7] if selected else (pins.get('v_release', '') if profile == 'stable' else '')
matches = passed and bool(expected) and expected in detail
check('V compiler', matches, detail, 'Select the tested V compiler from compatibility.json or rerun the setup helper.')
else:
check('V compiler', False, 'No V compiler found', 'Run the project setup helper, or pass --v with the tested compiler path.')
modules = modules or os.environ.get('VMODULES')
if modules:
for name, tag in pins.get('modules', {}).items():
checkout = Path(modules) / 'antono2' / name
head_ok, head = probe(['git', '-C', str(checkout), 'rev-parse', '--verify', 'HEAD'])
tag_ok, wanted = probe(['git', '-C', str(checkout), 'rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'])
check('Module ' + name, head_ok and tag_ok and head == wanted, f'{checkout}: expected {tag}',
'Preserve local changes and use a fresh module/cache directory with the declared releases.')
else:
check('Installed modules', False, 'VMODULES is not set', 'Pass --modules with the module directory used by setup.', optional=True)
sdk = sdk or os.environ.get('VULKAN_SDK')
needs_vulkan = 'ci_sdk' in pins or 'vulkan' in pins.get('modules', {})
if needs_vulkan and os.name == 'nt':
available = bool(sdk) and (Path(sdk) / 'Lib/vulkan-1.lib').is_file()
check('Vulkan SDK', available, str(sdk or 'Not configured'), 'Install the Vulkan SDK or pass --sdk with its directory.')
if runtime and needs_vulkan:
executable = shutil.which('vulkaninfo')
if not executable and sdk:
candidate = Path(sdk) / ('Bin/vulkaninfo.exe' if os.name == 'nt' else 'bin/vulkaninfo')
if candidate.is_file():
executable = str(candidate)
passed, detail = probe([executable, '--summary'], timeout=30) if executable else (False, 'vulkaninfo is unavailable')
check('Vulkan devices', passed, detail, 'Install Vulkan runtime tools and a compatible graphics driver; rerun --runtime.', optional=True)
return {'project': root.name, 'platform': os.name, 'profile': profile, 'compatibility': pins, 'checks': checks,
'hardware_note': 'Device enumeration does not validate hardware video decoding or OpenCL interoperability.'}


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--v', type=Path, help='Tested V compiler executable')
parser.add_argument('--modules', type=Path)
parser.add_argument('--sdk', type=Path)
parser.add_argument('--runtime', action='store_true', help='Also enumerate Vulkan devices, when tools are available')
parser.add_argument('--profile', choices=['stable', 'v3', 'release'], default='stable', help='Compiler profile to diagnose')
parser.add_argument('--json', action='store_true', help='Emit JSON without modifying the environment or caches')
args = parser.parse_args()
report = diagnose(Path(__file__).resolve().parents[1], args.v, args.modules, args.sdk, args.runtime, args.profile)
if args.json:
print(json.dumps(report, indent=2))
else:
for item in report['checks']:
print(f"{item['status'].upper()}: {item['name']}: {item['detail']}")
if item['action']:
print(' Next: ' + item['action'])
raise SystemExit(1 if any(item['status'] == 'fail' for item in report['checks']) else 0)


if __name__ == '__main__':
main()
39 changes: 39 additions & 0 deletions scripts/test_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Upgrades must keep declarations and immutable compiler pins in agreement."""
import json
from pathlib import Path
import shutil
import tempfile
import unittest
from compatibility import resolve


class CompatibilityChecks(unittest.TestCase):
def test_project_profile(self):
root = Path(__file__).resolve().parents[1]
pins = json.loads((root / 'compatibility.json').read_text())
values = resolve(root)
self.assertEqual(values['selected_v'], pins.get('v', pins.get('v_release', '')))
if 'v3_v' in pins:
self.assertEqual(resolve(root, 'v3')['selected_v'], pins['v3_v'])
self.assertEqual(resolve(root, 'master')['selected_v'], 'master')

def test_mutable_compiler_and_disagreement_rejected(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = Path(__file__).resolve().parents[1]
shutil.copyfile(source / 'v.mod', root / 'v.mod')
pins = json.loads((source / 'compatibility.json').read_text())
pins['v'] = 'master'
(root / 'compatibility.json').write_text(json.dumps(pins))
with self.assertRaisesRegex(ValueError, 'immutable'):
resolve(root)
del pins['v']
(root / 'v.mod').write_text("Module { dependencies: ['antono2.memory@v99.0.0'] }")
pins['modules']['memory'] = 'v1.4.0'
(root / 'compatibility.json').write_text(json.dumps(pins))
with self.assertRaisesRegex(ValueError, 'disagree'):
resolve(root)


if __name__ == '__main__':
unittest.main()
Loading
Loading