Skip to content

Commit

Permalink
Add machine-readable version list to bdist archives
Browse files Browse the repository at this point in the history
VERSIONS.md isn't meant to be machine-readable, but we've ended up parsing
it a couple times.  Add a versions.json file alongside it.

Signed-off-by: Benjamin Gilbert <bgilbert@cs.cmu.edu>
  • Loading branch information
bgilbert committed Mar 10, 2024
1 parent fb9e35c commit e8f9a80
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 30 deletions.
7 changes: 5 additions & 2 deletions artifacts/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ meson.add_dist_script(

artifacts = [
custom_target(
command : [find_program('write-project-versions.py'), '-o', '@OUTPUT@'],
output : 'VERSIONS.md',
command : [
find_program('write-project-versions.py'), '-j', '@OUTPUT0@',
'-m', '@OUTPUT1@',
],
output : ['versions.json', 'VERSIONS.md'],
# ensure we regenerate after dependency updates
build_always_stale : true,
env : env,
Expand Down
7 changes: 6 additions & 1 deletion artifacts/write-bdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ class Args(TypedArgs):
arcdir = arc.base / 'bin'
else:
arcdir = arc.base / 'lib'
elif name in ('CHANGELOG.md', 'VERSIONS.md', 'licenses'):
elif name in (
'CHANGELOG.md',
'VERSIONS.md',
'versions.json',
'licenses',
):
arcdir = arc.base
else:
arcdir = arc.base / 'bin'
Expand Down
49 changes: 35 additions & 14 deletions artifacts/write-project-versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@
from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys
from typing import TextIO

from common.argparse import TypedArgs
from common.meson import meson_host, meson_introspect
from common.software import write_project_versions
from common.software import (
Project,
Software,
Tool,
get_software_info,
write_version_markdown,
)

MINGW_VERSION_CHECK_HDR = b'''
#include <_mingw_mac.h>
Expand All @@ -40,47 +46,62 @@


class Args(TypedArgs):
output: TextIO
json: TextIO
markdown: TextIO


args = Args(
'write-project-versions', description='Write subproject version list.'
)
args.add_arg(
'-o',
'--output',
'-j',
'--json',
type=argparse.FileType('w'),
required=True,
help='output JSON',
)
args.add_arg(
'-m',
'--markdown',
type=argparse.FileType('w'),
default=sys.stdout,
help='output file',
required=True,
help='output Markdown',
)
args.parse()

env_info = {}
sw: list[Software] = list(Project.get_enabled())
compiler = meson_introspect('compilers')['host']['c']
if meson_host() == 'windows':
out = subprocess.Popen(
compiler['exelist'] + ['-E', '-'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
).communicate(MINGW_VERSION_CHECK_HDR)[0]
env_info['MinGW-w64'] = [
ver = [
line
for line in out.decode().split('\n')
if line.strip() and not line.startswith('#')
][0].replace('"', '')
sw.append(Tool(id='mingw-w64', display='MinGW-w64', version=ver))
if compiler['id'] == 'gcc':
ver = compiler['full_version']
match = re.match('[^ ]+ (.+)', ver)
if match:
ver = match[1]
env_info['GCC'] = ver
env_info['Binutils'] = (
sw.append(Tool(id='gcc', display='GCC', version=ver))
ver = (
subprocess.check_output([os.environ['LD'], '--version'])
.decode()
.split('\n')[0]
)
sw.append(Tool(id='binutils', display='Binutils', version=ver))
elif compiler['id'] == 'clang':
env_info['Clang'] = re.sub('.* version ', '', compiler['full_version'])
ver = re.sub('.* version ', '', compiler['full_version'])
sw.append(Tool(id='clang', display='Clang', version=ver))

with args.output as fh:
write_project_versions(fh, env_info)
info = get_software_info(sw)
with args.json as fh:
json.dump(info, fh, indent=2, sort_keys=True)
fh.write('\n')
with args.markdown as fh:
write_version_markdown(fh, info)
71 changes: 58 additions & 13 deletions common/software.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,62 @@

from __future__ import annotations

from abc import ABC
from collections.abc import Callable, Iterable
import configparser
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
import shutil
import subprocess
from typing import TextIO
from typing import Literal, TextIO, TypedDict

from .meson import meson_introspect, meson_source_root, parse_ini_file


class Infos(TypedDict):
versions: list[Info]


class Info(TypedDict):
id: str
display: str
version: str
type: SoftwareType


SoftwareType = Literal['primary', 'dependency', 'tool']


@dataclass
class Project:
class Software(ABC):
id: str
display: str

@property
def info(self) -> Info:
if isinstance(self, Tool):
typ: SoftwareType = 'tool'
elif isinstance(self, Project) and self.primary:
typ = 'primary'
else:
typ = 'dependency'
return {
'id': self.id,
'display': self.display,
# non-abstract subclasses have a version field or property
'version': self.version, # type: ignore[attr-defined]
'type': typ,
}


@dataclass
class Tool(Software):
version: str


@dataclass
class Project(Software):
licenses: Iterable[str | Callable[[Project], tuple[str, str]]]
primary: bool = False

Expand Down Expand Up @@ -228,9 +268,19 @@ def _sqlite3_license(proj: Project) -> tuple[str, str]:
_PROJECTS_IGNORE = {'gvdb'}


def write_project_versions(
fh: TextIO, env_info: None | dict[str, str] = None
) -> None:
def _sorted_infos(infos: list[Info]) -> list[Info]:
def key(info: Info) -> tuple[int, str]:
return (typ_map[info['type']], info['display'].lower())

typ_map = {'primary': 0, 'dependency': 1, 'tool': 2}
return sorted(infos, key=key)


def get_software_info(softwares: Iterable[Software]) -> Infos:
return {'versions': _sorted_infos([sw.info for sw in softwares])}


def write_version_markdown(fh: TextIO, infos: Infos) -> None:
def line(name: str, version: str, marker: str = '') -> None:
print(
'| {:20} | {:53} |'.format(
Expand All @@ -241,11 +291,6 @@ def line(name: str, version: str, marker: str = '') -> None:

line('Software', 'Version')
line('--------', '-------')

def key(proj: Project) -> tuple[int, str]:
return (0 if proj.primary else 1, proj.display.lower())

for proj in sorted(Project.get_enabled(), key=key):
line(proj.display, proj.version, '**' if proj.primary else '')
for software, version in sorted((env_info or {}).items()):
line(software, version, '_')
typ_map = {'primary': '**', 'dependency': '', 'tool': '_'}
for info in _sorted_infos(infos['versions']):
line(info['display'], info['version'], typ_map[info['type']])

0 comments on commit e8f9a80

Please sign in to comment.