Skip to content
Draft
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
4 changes: 4 additions & 0 deletions artifacts/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ env = environment()
env.prepend('PYTHONPATH', meson.project_source_root())
# Meson doesn't pass MESON_SOURCE_ROOT to custom targets
env.set('MESON_SOURCE_ROOT', meson.project_source_root())
env.set(
'MESONREWRITE',
'"@0@" rewrite'.format(find_program('meson').full_path()),
)
env.set('LD', find_program('ld').full_path())
if system == 'linux'
env.set('AUDITWHEEL', find_program('auditwheel').full_path())
Expand Down
18 changes: 12 additions & 6 deletions artifacts/postprocess-sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
sys.path.insert(0, os.environ['MESON_SOURCE_ROOT'])

from common.meson import meson_introspect, meson_source_root # noqa: E402
from common.python import pyproject_to_message # noqa: E402
from common.python import ( # noqa: E402
pyproject_fill_template,
pyproject_to_message,
)
from common.software import Project # noqa: E402

src = meson_source_root()
Expand All @@ -41,7 +44,7 @@
shutil.rmtree(dest / '.github')

# prune subproject directories to reduce tarball size
for proj in Project.get_all():
for proj in Project.get_enabled():
proj.prune_dist(dest)

# pin openslide-bin version suffix
Expand All @@ -52,11 +55,14 @@
suffix = ''
(dest / 'suffix').write_text(suffix + '\n')

# write licenses directory for Python source distribution
licensedir = dest / 'licenses'
for proj in Project.get_enabled():
proj.write_license_files(licensedir)

# create Python source distribution metadata
pyproject = (
(src / 'artifacts' / 'python' / 'pyproject.in.toml')
.read_text()
.replace('@version@', version)
pyproject = pyproject_fill_template(
(src / 'artifacts' / 'python' / 'pyproject.in.toml').read_text()
)
(dest / 'pyproject.toml').write_text(pyproject)
(dest / 'PKG-INFO').write_bytes(pyproject_to_message(pyproject).as_bytes())
15 changes: 9 additions & 6 deletions artifacts/python/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ py_config = {
}

py_artifacts = [
configure_file(
configuration : py_config,
input : 'pyproject.in.toml',
output : 'pyproject.toml',
),
configure_file(
configuration : py_config,
input : '__init__.in.py',
output : '__init__.py',
),
files('py.typed'),
custom_target(
command : [find_program('../write-pyproject.py'), '@INPUT@', '@OUTPUT@'],
input : 'pyproject.in.toml',
output : 'pyproject.toml',
# ensure we regenerate after dependency updates and version bumps
build_always_stale : true,
env : env,
),
files(meson.project_source_root() / 'COPYING.LESSER', 'py.typed'),
libopenslide_postprocessed,
licenses,
]
4 changes: 2 additions & 2 deletions artifacts/python/pyproject.in.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ maintainers = [
]
description = "Binary build of OpenSlide"
readme = "artifacts/python/README.md"
license = {text = "GNU Lesser General Public License, version 2.1"}
license = "@spdx@"
license-files = "@license-files@"
keywords = ["OpenSlide", "whole-slide image", "virtual slide", "library"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Healthcare Industry",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
Expand Down
2 changes: 1 addition & 1 deletion artifacts/write-licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ class Args(TypedArgs):
if args.dir.exists():
shutil.rmtree(args.dir)
for proj in Project.get_enabled():
proj.write_licenses(args.dir / proj.display)
proj.write_license_files(args.dir)
51 changes: 51 additions & 0 deletions artifacts/write-pyproject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
#
# Tools for building OpenSlide and its dependencies
#
# Copyright (c) 2023-2025 Benjamin Gilbert
# All rights reserved.
#
# This script is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License, version 2.1,
# as published by the Free Software Foundation.
#
# This script is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this script. If not, see <http://www.gnu.org/licenses/>.
#

from __future__ import annotations

import argparse
from typing import TextIO

from common.argparse import TypedArgs
from common.python import pyproject_fill_template


class Args(TypedArgs):
input: TextIO
output: TextIO


args = Args('write-pyproject', description='Write pyproject.toml.')
args.add_arg(
'input',
type=argparse.FileType('r'),
help='input template file',
)
args.add_arg(
'output',
type=argparse.FileType('w'),
help='output file',
)
args.parse()

with args.input:
pyproject = pyproject_fill_template(args.input.read())
with args.output:
args.output.write(pyproject)
12 changes: 10 additions & 2 deletions artifacts/write-wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,16 @@ class Args(TypedArgs):
whl.metadir / 'METADATA', BytesIO(meta.as_bytes())
)
)
elif path.name == 'licenses':
whl.add_tree(whl.metadir, path)
elif path.name in ('COPYING.LESSER', 'licenses'):
# Assume the file or dir is in the root of the sdist.
# Write licenses directory to {metadir}/licenses/licenses,
# as required by PEP 639.
if path.is_dir():
whl.add_tree(whl.metadir / 'licenses', path)
else:
whl.add(
FileMember(whl.metadir / 'licenses' / path.name, fh)
)
else:
name = re.sub('(\\.so\\.[0-9]+)\\.[0-9.]+', '\\1', path.name)
whl.add(FileMember(whl.datadir / name, fh))
Expand Down
4 changes: 2 additions & 2 deletions bintool
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ from common.meson import (
)
from common.software import Project

WINDOWS_API_VERS = (4, 5, 6)
LINUX_API_VERS = (4, 5)
WINDOWS_API_VERS = (7,)
LINUX_API_VERS = (6,)
# we have a higher minimum than the underlying meson.build
MESON_MIN_VER = (1, 5, 0)

Expand Down
48 changes: 43 additions & 5 deletions common/python.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# Tools for building OpenSlide and its dependencies
#
# Copyright (c) 2023 Benjamin Gilbert
# Copyright (c) 2023-2025 Benjamin Gilbert
# All rights reserved.
#
# This script is free software: you can redistribute it and/or modify it
Expand All @@ -21,15 +21,47 @@

from email.message import Message
from email.policy import Compat32
from pathlib import PurePath
import re
import tomllib

from .meson import meson_source_root
from .meson import meson_introspect, meson_source_root
from .software import Project, get_spdx


def pyproject_fill_template(tmpl: str) -> str:
version: str = meson_introspect('projectinfo')['version']
projs = Project.get_enabled()
license_relpaths = [PurePath('COPYING.LESSER')] + [
PurePath('licenses') / relpath
for proj in projs
for relpath in proj.license_relpaths
]
for relpath in license_relpaths:
# we don't do proper toml assembly
if '"' in relpath.as_posix():
raise ValueError(f'Invalid license file path: {relpath}')
return (
tmpl.replace('@version@', version)
.replace('@spdx@', get_spdx(projs))
.replace(
'"@license-files@"',
'["'
+ '", "'.join(
sorted(
(relpath.as_posix() for relpath in license_relpaths),
key=lambda s: s.lower(),
)
)
+ '"]',
)
)


def pyproject_to_message(pyproject: str) -> Message:
meta = tomllib.loads(pyproject)
out = Message(policy=Compat32(max_line_length=None))
out['Metadata-Version'] = '2.3'
out['Metadata-Version'] = '2.4'
for k, v in meta['project'].items():
k = k.lower()
if k == 'name':
Expand Down Expand Up @@ -62,8 +94,14 @@ def pyproject_to_message(pyproject: str) -> Message:
for item in v:
out['Maintainer-Email'] = f'{item["name"]} <{item["email"]}>'
elif k == 'license':
out['License'] = v.pop('text')
assert not v
out['License-Expression'] = v
elif k == 'license-files':
for vv in v:
if re.search(r'\*|\?|\[|\.\.', vv):
raise ValueError(
f'Glob or path traversal in license file path: {vv}'
)
out['License-File'] = vv
elif k == 'classifiers':
for vv in v:
out['Classifier'] = vv
Expand Down
Loading