-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathdistributions.py
186 lines (151 loc) · 5.5 KB
/
distributions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""Logic for generating distributions from a :ref:`Project`."""
import io
import os
import posixpath
import subprocess
import tarfile
import textwrap
from pathlib import Path
from typing import Callable, Optional, Tuple
from .errors import DiagnosticError
from .nodejs import generate_assets
from .project import Project
from .wheelfile import WheelFile, include_parent_paths
def _get_vcs_tracked_names(path: Path) -> Optional[Tuple[str, ...]]:
if not (path / ".git").is_dir():
return None
outb = subprocess.check_output(
["git", "ls-files", "--recurse-submodules", "-z"],
cwd=path,
)
tracked_files = [
os.fsdecode(location) for location in outb.strip(b"\0").split(b"\0") if location
]
return include_parent_paths(tracked_files)
def _sdist_filter(
project: Project, base: str
) -> Callable[[tarfile.TarInfo], Optional[tarfile.TarInfo]]:
"""Create a filter to pass to tarfile.add, for this project."""
compiled_assets = project.compiled_assets
tracked_names = _get_vcs_tracked_names(project.location)
def _filter(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
# Include the entry for the root.
if tarinfo.name == base:
return tarinfo
name = tarinfo.name[len(base) + 1 :]
# Exclude hidden files
if name.startswith("."):
return None
# Exclude compiled pyc files.
if posixpath.basename(name) == "__pycache__":
return None
# Exclude compiled assets.
if name in compiled_assets:
return None
# Exclude things that are excluded from version control.
if tracked_names is not None and name not in tracked_names:
return None
return tarinfo
# NOTE: The CLI for build needs to check that the compiled assets we have here, are
# not tracked under version control.
return _filter
#
# External API
#
def generate_source_distribution(
project: Project,
*,
destination: Path,
) -> str:
"""Generate a source distribution for project, and place it inside destination.
:return: Name of the generated source tarball.
"""
os.makedirs(destination, exist_ok=True)
dashed_pair = f"{project.snake_name}-{project.metadata.version}"
sdist_tarball = destination / f"{dashed_pair}.tar.gz"
# NOTE: Post Python 3.7 -- can drop the format kwarg, since it'll be the default.
with tarfile.open(
name=sdist_tarball, mode="w:gz", format=tarfile.PAX_FORMAT
) as tarball:
# Recursively add the files to this tarball, with an exclusion filter.
tarball.add(
project.location,
arcname=dashed_pair,
recursive=True,
filter=_sdist_filter(project, dashed_pair),
)
# Write the metadata file.
metadata_content = project.get_metadata_file_contents().encode()
tarinfo = tarfile.TarInfo(posixpath.join(dashed_pair, "PKG-INFO"))
tarinfo.size = len(metadata_content)
tarball.addfile(tarinfo, io.BytesIO(metadata_content))
return sdist_tarball.name
def generate_metadata(
project: Project,
*,
destination: Path,
) -> str:
"""Generate the metadata (.dist-info) and place it in `metadata_directory`.
:return: name of the dist-info directory generated.
"""
dist_info = (
destination / f"{project.snake_name}-{project.metadata.version}.dist-info"
)
try:
os.makedirs(dist_info)
except OSError as error:
raise DiagnosticError(
message="Metadata directory already exists",
context=None,
hint_stmt=None,
) from error
# Delegated generation.
(dist_info / "entry_points.txt").write_text(project.get_entry_points_contents())
(dist_info / "LICENSE").write_text(project.get_license_contents())
(dist_info / "METADATA").write_text(project.get_metadata_file_contents())
# Templated generation.
(dist_info / "WHEEL").write_text(
textwrap.dedent(
"""\
Wheel-Version: 1.0
Generator: flit {version}
Root-Is-Purelib: true
Tag: py3-none-any
"""
)
)
return dist_info.name
def generate_wheel_distribution(
project: Project,
*,
destination: Path,
metadata_directory: Path,
editable: bool,
) -> str:
# Generate the JS / CSS assets
generate_assets(project, production=not editable)
wheel_path = (
destination
/ f"{project.snake_name}-{project.metadata.version}-py3-none-any.whl"
)
tracked_names = _get_vcs_tracked_names(project.location)
with WheelFile(
path=wheel_path,
compiled_assets=project.compiled_assets,
tracked_names=tracked_names,
) as wheel:
if editable:
# Generate a .pth file, for editable installs. There's an enforced src/
# directory, so this is fairly safe to do.
wheel.add_string(
os.fsdecode(project.source_path.resolve()),
dest=project.snake_name + ".pth",
)
else:
# Add files from the project's src/ directory.
wheel.add_directory(project.source_path, dest="", base=project.location)
# Put the metadata at the end.
# https://www.python.org/dev/peps/pep-0427/#recommended-archiver-features
wheel.add_directory(metadata_directory, dest=metadata_directory.name, base=None)
wheel.write_record(dest=metadata_directory.name + "/RECORD")
return wheel.name