Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mesonpy: include uncommited changes in sdist #58

Merged
merged 1 commit into from
May 26, 2022
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
47 changes: 35 additions & 12 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import collections
import contextlib
import functools
import io
import itertools
import json
import os
Expand All @@ -23,6 +24,7 @@
import subprocess
import sys
import sysconfig
import tarfile
import tempfile
import textwrap
import typing
Expand Down Expand Up @@ -538,7 +540,8 @@ def version(self) -> str:
assert isinstance(version, str)
return version

@property
@property # type: ignore[misc]
@functools.lru_cache(maxsize=1)
def metadata(self) -> bytes: # noqa: C901
"""Project metadata."""
# the rest of the keys are only available when using PEP 621 metadata
Expand Down Expand Up @@ -718,26 +721,46 @@ def _select_abi_tag(self) -> Optional[mesonpy._tags.Tag]: # noqa: C901
def sdist(self, directory: Path) -> pathlib.Path:
"""Generates a sdist (source distribution) in the specified directory."""
# generate meson dist file
self._meson('dist', '--no-tests', '--formats', 'gztar')
self._meson('dist', '--allow-dirty', '--no-tests', '--formats', 'gztar')

# move meson dist file to output path
dist_name = f'{self.name}-{self.version}'
meson_dist_name = f'{self._meson_name}-{self._meson_version}'
meson_dist = pathlib.Path(self._build_dir, 'meson-dist', f'{meson_dist_name}.tar.gz')
meson_dist_path = pathlib.Path(self._build_dir, 'meson-dist', f'{meson_dist_name}.tar.gz')
sdist = pathlib.Path(directory, f'{dist_name}.tar.gz')

with mesonpy._util.edit_targz(meson_dist, sdist) as content:
# rename from meson name to sdist name if necessary
if dist_name != meson_dist_name:
shutil.move(str(content / meson_dist_name), str(content / dist_name))
with tarfile.open(meson_dist_path, 'r:gz') as meson_dist, mesonpy._util.create_targz(sdist) as (tar, mtime):
for member in meson_dist.getmembers():
# skip the generated meson native file
if member.name == f'{meson_dist_name}/.mesonpy-native-file.ini':
continue

# calculate the file path in the source directory
assert member.name, member.name
member_parts = member.name.split('/')
if len(member_parts) <= 1:
continue
path = self._source_dir.joinpath(*member_parts[1:])

if not path.is_file():
continue

# rewrite the path if necessary, to match the sdist distribution name
if dist_name != meson_dist_name:
member.name = path.relative_to(self._source_dir).as_posix()

# rewrite the size
member.size = os.path.getsize(path)

# remove .mesonpy-native-file.ini if it exists
native_file = content / meson_dist_name / '.mesonpy-native-file.ini'
if native_file.exists():
native_file.unlink()
with path.open('rb') as f:
tar.addfile(member, fileobj=f)

# add PKG-INFO to dist file to make it a sdist
content.joinpath(dist_name, 'PKG-INFO').write_bytes(self.metadata)
pkginfo_info = tarfile.TarInfo(f'{dist_name}/PKG-INFO')
if mtime:
pkginfo_info.mtime = mtime
pkginfo_info.size = len(self.metadata) # type: ignore[arg-type]
tar.addfile(pkginfo_info, fileobj=io.BytesIO(self.metadata)) # type: ignore[arg-type]

return sdist

Expand Down
51 changes: 20 additions & 31 deletions mesonpy/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import contextlib
import gzip
import os
import pathlib
import sys
import tarfile
import tempfile
import typing

from typing import IO
from typing import IO, Optional, Tuple

from mesonpy._compat import Iterable, Iterator, Path

Expand Down Expand Up @@ -41,35 +39,26 @@ def add_ld_path(paths: Iterable[str]) -> Iterator[None]:


@contextlib.contextmanager
def edit_targz(path: Path, new_path: Path) -> Iterator[pathlib.Path]:
def create_targz(path: Path) -> Iterator[Tuple[tarfile.TarFile, Optional[int]]]:
"""Opens a .tar.gz file in the file system for edition.."""
with tempfile.TemporaryDirectory(prefix='mesonpy-') as tmpdir:
workdir = pathlib.Path(tmpdir)
with tarfile.open(path, 'r:gz') as tar:
tar.extractall(tmpdir)

yield workdir

# reproducibility
source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
mtime = int(source_date_epoch) if source_date_epoch else None

file = typing.cast(IO[bytes], gzip.GzipFile(
os.path.join(path, new_path),
mode='wb',
mtime=mtime,
))
with contextlib.closing(file), tarfile.TarFile(
mode='w',
fileobj=file,
format=tarfile.PAX_FORMAT, # changed in 3.8 to GNU
) as tar:
for path in workdir.rglob('*'):
if path.is_file():
tar.add(
name=path,
arcname=path.relative_to(workdir).as_posix(),
)

# reproducibility
source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
mtime = int(source_date_epoch) if source_date_epoch else None

file = typing.cast(IO[bytes], gzip.GzipFile(
os.path.join(path, path),
mode='wb',
mtime=mtime,
))
tar = tarfile.TarFile(
mode='w',
fileobj=file,
format=tarfile.PAX_FORMAT, # changed in 3.8 to GNU
)

with contextlib.closing(file), tar:
yield tar, mtime


class CLICounter:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
build-backend = 'mesonpy'
backend-path = ['.']
requires = [
'meson>=0.60.0',
'meson>=0.62.0',
'ninja',
'pep621>=0.3.0',
'tomli>=1.0.0',
Expand Down
14 changes: 14 additions & 0 deletions tests/packages/subdirs/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
project(
'subdirs',
version: '1.0.0',
)

py_mod = import('python')
py = py_mod.find_installation()

py.install_sources([
'subdirs' / '__init__.py',
'subdirs' / 'a' / '__init__.py',
'subdirs' / 'a' / 'b' / 'c.py',
'subdirs' / 'b' / 'c.py',
])
3 changes: 3 additions & 0 deletions tests/packages/subdirs/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
Empty file.
Empty file.
Empty file.
Empty file.
51 changes: 51 additions & 0 deletions tests/test_sdist.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# SPDX-License-Identifier: EUPL-1.2

import os
import tarfile
import textwrap

import mesonpy

from .conftest import in_git_repo_context


def test_contents(sdist_library):
Expand All @@ -14,3 +20,48 @@ def test_contents(sdist_library):
'library-1.0.0/pyproject.toml',
'library-1.0.0/PKG-INFO',
}


def test_contents_subdirs(sdist_subdirs):
sdist = tarfile.open(sdist_subdirs, 'r:gz')

assert set(sdist.getnames()) == {
'subdirs-1.0.0/PKG-INFO',
'subdirs-1.0.0/meson.build',
'subdirs-1.0.0/pyproject.toml',
'subdirs-1.0.0/subdirs/__init__.py',
'subdirs-1.0.0/subdirs/a/__init__.py',
'subdirs-1.0.0/subdirs/a/b/c.py',
'subdirs-1.0.0/subdirs/b/c.py',
}


def test_contents_unstaged(package_pure, tmpdir):
new_data = textwrap.dedent('''
def bar():
return 'foo'
''').strip()

with open('pure.py', 'r') as f:
old_data = f.read()

try:
with in_git_repo_context(), open('pure.py', 'w') as f, open('crap', 'x'):
f.write(new_data)

sdist_path = mesonpy.build_sdist(os.fspath(tmpdir))
finally:
with open('pure.py', 'w') as f:
f.write(old_data)
os.unlink('crap')

sdist = tarfile.open(tmpdir / sdist_path, 'r:gz')

assert set(sdist.getnames()) == {
'pure-1.0.0/PKG-INFO',
'pure-1.0.0/meson.build',
'pure-1.0.0/pure.py',
'pure-1.0.0/pyproject.toml',
}
read_data = sdist.extractfile('pure-1.0.0/pure.py').read().replace(b'\r\n', b'\n')
assert read_data == new_data.encode()