mirror of https://github.com/pypa/hatch.git
861 lines
31 KiB
Python
861 lines
31 KiB
Python
import os
|
|
import re
|
|
from collections import OrderedDict
|
|
from contextlib import contextmanager
|
|
from io import open
|
|
|
|
import pathspec
|
|
|
|
from ...utils.fs import locate_file
|
|
from ..constants import DEFAULT_BUILD_DIRECTORY, BuildEnvVars
|
|
|
|
|
|
class IncludedFile(object):
|
|
__slots__ = ('path', 'relative_path', 'distribution_path')
|
|
|
|
def __init__(self, path, relative_path, distribution_path):
|
|
self.path = path
|
|
self.relative_path = relative_path
|
|
self.distribution_path = distribution_path
|
|
|
|
|
|
class BuilderInterface(object):
|
|
"""
|
|
Example usage:
|
|
|
|
=== ":octicons-file-code-16: plugin.py"
|
|
|
|
```python
|
|
from hatchling.builders.plugin.interface import BuilderInterface
|
|
|
|
|
|
class SpecialBuilder(BuilderInterface):
|
|
PLUGIN_NAME = 'special'
|
|
...
|
|
```
|
|
|
|
=== ":octicons-file-code-16: hooks.py"
|
|
|
|
```python
|
|
from hatchling.plugin import hookimpl
|
|
|
|
from .plugin import SpecialBuilder
|
|
|
|
|
|
@hookimpl
|
|
def hatch_register_builder():
|
|
return SpecialBuilder
|
|
```
|
|
"""
|
|
|
|
PLUGIN_NAME = ''
|
|
"""The name used for selection."""
|
|
|
|
def __init__(self, root, plugin_manager=None, config=None, metadata=None, app=None):
|
|
self.__root = root
|
|
self.__plugin_manager = plugin_manager
|
|
self.__config = config
|
|
self.__metadata = metadata
|
|
self.__app = app
|
|
self.__project_config = None
|
|
self.__hatch_config = None
|
|
self.__build_config = None
|
|
self.__build_targets = None
|
|
self.__target_config = None
|
|
self.__hook_config = None
|
|
self.__packages = None
|
|
self.__versions = None
|
|
self.__dependencies = None
|
|
|
|
# Possible pathspec.PathSpec
|
|
self.__include_spec = None
|
|
self.__exclude_spec = None
|
|
self.__artifact_spec = None
|
|
self.build_artifact_spec = None
|
|
|
|
# These are used to create the pathspecs and will never be `None` after the first match attempt
|
|
self.__include_patterns = None
|
|
self.__exclude_patterns = None
|
|
self.__artifact_patterns = None
|
|
|
|
# Common options
|
|
self.__directory = None
|
|
self.__ignore_vcs = None
|
|
self.__reproducible = None
|
|
self.__dev_mode_dirs = None
|
|
|
|
# Metadata
|
|
self.__project_id = None
|
|
|
|
def build(self, directory=None, versions=None, clean=None, hooks_only=None, no_hooks=None, clean_only=False):
|
|
if directory is None:
|
|
if BuildEnvVars.LOCATION in os.environ:
|
|
directory = self._normalize_build_directory(os.environ[BuildEnvVars.LOCATION])
|
|
else:
|
|
directory = self.directory
|
|
|
|
if not os.path.isdir(directory):
|
|
os.makedirs(directory)
|
|
|
|
version_api = self.get_version_api()
|
|
|
|
if not versions:
|
|
versions = self.versions
|
|
else:
|
|
unknown_versions = set(versions) - set(version_api)
|
|
if unknown_versions:
|
|
raise ValueError(
|
|
'Unknown versions for target `{}`: {}'.format(
|
|
self.PLUGIN_NAME, ', '.join(map(str, sorted(unknown_versions)))
|
|
)
|
|
)
|
|
|
|
if hooks_only is None:
|
|
hooks_only = _env_var_enabled(BuildEnvVars.HOOKS_ONLY)
|
|
|
|
if no_hooks is None:
|
|
no_hooks = _env_var_enabled(BuildEnvVars.NO_HOOKS)
|
|
if no_hooks:
|
|
configured_build_hooks = OrderedDict()
|
|
else:
|
|
configured_build_hooks = self.get_build_hooks(directory)
|
|
|
|
build_hooks = list(configured_build_hooks.values())
|
|
|
|
if clean_only:
|
|
clean = True
|
|
elif clean is None:
|
|
clean = _env_var_enabled(BuildEnvVars.CLEAN)
|
|
if clean:
|
|
if not hooks_only:
|
|
self.clean(directory, versions)
|
|
if not no_hooks:
|
|
for build_hook in build_hooks:
|
|
build_hook.clean(versions)
|
|
|
|
if clean_only:
|
|
return
|
|
|
|
for version in versions:
|
|
self.app.display_debug('Building `{}` version `{}`'.format(self.PLUGIN_NAME, version))
|
|
|
|
build_data = self.get_default_build_data()
|
|
|
|
# Make sure reserved fields are set
|
|
build_data.setdefault('artifacts', [])
|
|
|
|
# Pass all the configured build hooks for future unforeseen scenarios needing ultimate control
|
|
build_data['configured_build_hooks'] = configured_build_hooks
|
|
|
|
# Execute all `initialize` build hooks
|
|
for build_hook in build_hooks:
|
|
build_hook.initialize(version, build_data)
|
|
|
|
if hooks_only:
|
|
self.app.display_debug('Only ran build hooks for `{}` version `{}`'.format(self.PLUGIN_NAME, version))
|
|
continue
|
|
|
|
# Build the artifact
|
|
with self.configure_with_build_data(build_data):
|
|
artifact = version_api[version](directory, **build_data)
|
|
|
|
# Execute all `finalize` build hooks
|
|
for build_hook in build_hooks:
|
|
build_hook.finalize(version, build_data, artifact)
|
|
|
|
yield artifact
|
|
|
|
def recurse_project_files(self):
|
|
"""
|
|
Returns a consistently generated series of file objects for every file that should be distributed. Each file
|
|
object has three `str` attributes:
|
|
|
|
- `path` - the absolute path
|
|
- `relative_path` - the path relative to the project root
|
|
- `distribution_path` - the path to be distributed as
|
|
"""
|
|
for root, dirs, files in os.walk(self.root):
|
|
relative_path = os.path.relpath(root, self.root)
|
|
|
|
# First iteration
|
|
if relative_path == '.':
|
|
relative_path = ''
|
|
|
|
dirs[:] = sorted(
|
|
d
|
|
for d in dirs
|
|
if not (
|
|
self.ignore_directory(d)
|
|
# The trailing slash is necessary so e.g. `bar/` matches `foo/bar`
|
|
or self.path_is_excluded('{}/'.format(os.path.join(relative_path, d)))
|
|
)
|
|
)
|
|
|
|
if self.ignore_files(files):
|
|
continue
|
|
|
|
for f in sorted(files):
|
|
relative_file_path = os.path.join(relative_path, f)
|
|
if self.include_path(relative_file_path):
|
|
yield IncludedFile(
|
|
os.path.join(root, f), relative_file_path, self.get_distribution_path(relative_file_path)
|
|
)
|
|
|
|
def include_path(self, relative_path):
|
|
return (
|
|
self.path_is_build_artifact(relative_path)
|
|
or self.path_is_artifact(relative_path)
|
|
or (self.path_is_included(relative_path) and not self.path_is_excluded(relative_path))
|
|
)
|
|
|
|
def path_is_included(self, relative_path):
|
|
if self.include_spec is None:
|
|
return True
|
|
|
|
return self.include_spec.match_file(relative_path)
|
|
|
|
def path_is_excluded(self, relative_path):
|
|
if self.exclude_spec is None:
|
|
return False
|
|
|
|
return self.exclude_spec.match_file(relative_path)
|
|
|
|
def path_is_artifact(self, relative_path):
|
|
if self.artifact_spec is None:
|
|
return False
|
|
|
|
return self.artifact_spec.match_file(relative_path)
|
|
|
|
def path_is_build_artifact(self, relative_path):
|
|
if self.build_artifact_spec is None:
|
|
return False
|
|
|
|
return self.build_artifact_spec.match_file(relative_path)
|
|
|
|
@property
|
|
def include_spec(self):
|
|
if self.__include_patterns is None:
|
|
if 'include' in self.target_config:
|
|
include_config = self.target_config
|
|
include_location = 'tool.hatch.build.targets.{}.include'.format(self.PLUGIN_NAME)
|
|
else:
|
|
include_config = self.build_config
|
|
include_location = 'tool.hatch.build.include'
|
|
|
|
all_include_patterns = []
|
|
|
|
include_patterns = include_config.get('include', self.default_include_patterns())
|
|
if isinstance(include_patterns, str):
|
|
for include_pattern in include_patterns.split(','):
|
|
if include_pattern:
|
|
all_include_patterns.append(include_pattern)
|
|
elif isinstance(include_patterns, list):
|
|
for i, include_pattern in enumerate(include_patterns, 1):
|
|
if not isinstance(include_pattern, str):
|
|
raise TypeError('Pattern #{} in field `{}` must be a string'.format(i, include_location))
|
|
|
|
if include_pattern:
|
|
all_include_patterns.append(include_pattern)
|
|
else:
|
|
raise TypeError(
|
|
'Field `{}` must be a comma-separated string or an array of strings'.format(include_location)
|
|
)
|
|
|
|
for relative_path, _ in self.packages:
|
|
# Matching only at the root requires a forward slash, back slashes do not work. As such,
|
|
# normalize to forward slashes for consistency.
|
|
all_include_patterns.append('/{}'.format(relative_path.replace(os.path.sep, '/')))
|
|
|
|
if all_include_patterns:
|
|
self.__include_spec = pathspec.PathSpec.from_lines(
|
|
pathspec.patterns.GitWildMatchPattern, all_include_patterns
|
|
)
|
|
|
|
self.__include_patterns = all_include_patterns
|
|
|
|
return self.__include_spec
|
|
|
|
@property
|
|
def exclude_spec(self):
|
|
if self.__exclude_patterns is None:
|
|
if 'exclude' in self.target_config:
|
|
exclude_config = self.target_config
|
|
exclude_location = 'tool.hatch.build.targets.{}.exclude'.format(self.PLUGIN_NAME)
|
|
else:
|
|
exclude_config = self.build_config
|
|
exclude_location = 'tool.hatch.build.exclude'
|
|
|
|
all_exclude_patterns = self.default_global_exclude_patterns()
|
|
|
|
exclude_patterns = exclude_config.get('exclude', self.default_exclude_patterns())
|
|
if isinstance(exclude_patterns, str):
|
|
for exclude_pattern in exclude_patterns.split(','):
|
|
if exclude_pattern:
|
|
all_exclude_patterns.append(exclude_pattern)
|
|
elif isinstance(exclude_patterns, list):
|
|
for i, exclude_pattern in enumerate(exclude_patterns, 1):
|
|
if not isinstance(exclude_pattern, str):
|
|
raise TypeError('Pattern #{} in field `{}` must be a string'.format(i, exclude_location))
|
|
|
|
if exclude_pattern:
|
|
all_exclude_patterns.append(exclude_pattern)
|
|
else:
|
|
raise TypeError(
|
|
'Field `{}` must be a comma-separated string or an array of strings'.format(exclude_location)
|
|
)
|
|
|
|
if not self.ignore_vcs:
|
|
all_exclude_patterns.extend(self.load_vcs_ignore_patterns())
|
|
|
|
if all_exclude_patterns:
|
|
self.__exclude_spec = pathspec.PathSpec.from_lines(
|
|
pathspec.patterns.GitWildMatchPattern, all_exclude_patterns
|
|
)
|
|
|
|
self.__exclude_patterns = all_exclude_patterns
|
|
|
|
return self.__exclude_spec
|
|
|
|
@property
|
|
def artifact_spec(self):
|
|
if self.__artifact_patterns is None:
|
|
if 'artifacts' in self.target_config:
|
|
artifact_config = self.target_config
|
|
artifact_location = 'tool.hatch.build.targets.{}.artifacts'.format(self.PLUGIN_NAME)
|
|
else:
|
|
artifact_config = self.build_config
|
|
artifact_location = 'tool.hatch.build.artifacts'
|
|
|
|
all_artifact_patterns = []
|
|
|
|
artifact_patterns = artifact_config.get('artifacts', [])
|
|
if isinstance(artifact_patterns, str):
|
|
for artifact_pattern in artifact_patterns.split(','):
|
|
if artifact_pattern:
|
|
all_artifact_patterns.append(artifact_pattern)
|
|
elif isinstance(artifact_patterns, list):
|
|
for i, artifact_pattern in enumerate(artifact_patterns, 1):
|
|
if not isinstance(artifact_pattern, str):
|
|
raise TypeError('Pattern #{} in field `{}` must be a string'.format(i, artifact_location))
|
|
|
|
if artifact_pattern:
|
|
all_artifact_patterns.append(artifact_pattern)
|
|
else:
|
|
raise TypeError(
|
|
'Field `{}` must be a comma-separated string or an array of strings'.format(artifact_location)
|
|
)
|
|
|
|
if all_artifact_patterns:
|
|
self.__artifact_spec = pathspec.PathSpec.from_lines(
|
|
pathspec.patterns.GitWildMatchPattern, all_artifact_patterns
|
|
)
|
|
|
|
self.__artifact_patterns = all_artifact_patterns
|
|
|
|
return self.__artifact_spec
|
|
|
|
@property
|
|
def root(self):
|
|
"""
|
|
The root of the project tree.
|
|
"""
|
|
return self.__root
|
|
|
|
@property
|
|
def plugin_manager(self):
|
|
if self.__plugin_manager is None:
|
|
from ...plugin.manager import PluginManager
|
|
|
|
self.__plugin_manager = PluginManager()
|
|
|
|
return self.__plugin_manager
|
|
|
|
@property
|
|
def metadata(self):
|
|
if self.__metadata is None:
|
|
from ...metadata.core import ProjectMetadata
|
|
|
|
self.__metadata = ProjectMetadata(self.root, self.plugin_manager, self.__config)
|
|
|
|
return self.__metadata
|
|
|
|
@property
|
|
def app(self):
|
|
"""
|
|
An instance of [Application](utilities.md#hatchling.bridge.app.Application).
|
|
"""
|
|
if self.__app is None:
|
|
from ...bridge.app import Application
|
|
|
|
self.__app = Application().get_safe_application()
|
|
|
|
return self.__app
|
|
|
|
@property
|
|
def config(self):
|
|
if self.__config is None:
|
|
self.__config = self.metadata.config
|
|
|
|
return self.__config
|
|
|
|
@property
|
|
def project_config(self):
|
|
if self.__project_config is None:
|
|
self.__project_config = self.metadata.core.config
|
|
|
|
return self.__project_config
|
|
|
|
@property
|
|
def hatch_config(self):
|
|
if self.__hatch_config is None:
|
|
self.__hatch_config = self.metadata.hatch.config
|
|
|
|
return self.__hatch_config
|
|
|
|
@property
|
|
def build_config(self):
|
|
"""
|
|
=== ":octicons-file-code-16: pyproject.toml"
|
|
|
|
```toml
|
|
[tool.hatch.build]
|
|
```
|
|
|
|
=== ":octicons-file-code-16: hatch.toml"
|
|
|
|
```toml
|
|
[build]
|
|
```
|
|
"""
|
|
if self.__build_config is None:
|
|
self.__build_config = self.metadata.hatch.build_config
|
|
|
|
return self.__build_config
|
|
|
|
@property
|
|
def target_config(self):
|
|
"""
|
|
=== ":octicons-file-code-16: pyproject.toml"
|
|
|
|
```toml
|
|
[tool.hatch.build.targets.<PLUGIN_NAME>]
|
|
```
|
|
|
|
=== ":octicons-file-code-16: hatch.toml"
|
|
|
|
```toml
|
|
[build.targets.<PLUGIN_NAME>]
|
|
```
|
|
"""
|
|
if self.__target_config is None:
|
|
target_config = self.metadata.hatch.build_targets.get(self.PLUGIN_NAME, {})
|
|
if not isinstance(target_config, dict):
|
|
raise TypeError('Field `tool.hatch.build.targets.{}` must be a table'.format(self.PLUGIN_NAME))
|
|
|
|
self.__target_config = target_config
|
|
|
|
return self.__target_config
|
|
|
|
@property
|
|
def hook_config(self):
|
|
if self.__hook_config is None:
|
|
hook_config = OrderedDict()
|
|
|
|
target_hook_config = self.target_config.get('hooks', {})
|
|
if not isinstance(target_hook_config, dict):
|
|
raise TypeError('Field `tool.hatch.build.targets.{}.hooks` must be a table'.format(self.PLUGIN_NAME))
|
|
|
|
for hook_name, config in target_hook_config.items():
|
|
if not isinstance(config, dict):
|
|
raise TypeError(
|
|
'Field `tool.hatch.build.targets.{}.hooks.{}` must be a table'.format(
|
|
self.PLUGIN_NAME, hook_name
|
|
)
|
|
)
|
|
|
|
hook_config[hook_name] = config
|
|
|
|
global_hook_config = self.build_config.get('hooks', {})
|
|
if not isinstance(global_hook_config, dict):
|
|
raise TypeError('Field `tool.hatch.build.hooks` must be a table')
|
|
|
|
for hook_name, config in global_hook_config.items():
|
|
if not isinstance(config, dict):
|
|
raise TypeError('Field `tool.hatch.build.hooks.{}` must be a table'.format(hook_name))
|
|
|
|
hook_config.setdefault(hook_name, config)
|
|
|
|
self.__hook_config = hook_config
|
|
|
|
return self.__hook_config
|
|
|
|
@property
|
|
def directory(self):
|
|
if self.__directory is None:
|
|
if 'directory' in self.target_config:
|
|
directory = self.target_config['directory']
|
|
if not isinstance(directory, str):
|
|
raise TypeError(
|
|
'Field `tool.hatch.build.targets.{}.directory` must be a string'.format(self.PLUGIN_NAME)
|
|
)
|
|
else:
|
|
directory = self.build_config.get('directory', DEFAULT_BUILD_DIRECTORY)
|
|
if not isinstance(directory, str):
|
|
raise TypeError('Field `tool.hatch.build.directory` must be a string')
|
|
|
|
self.__directory = self._normalize_build_directory(directory)
|
|
|
|
return self.__directory
|
|
|
|
@property
|
|
def ignore_vcs(self):
|
|
if self.__ignore_vcs is None:
|
|
if 'ignore-vcs' in self.target_config:
|
|
ignore_vcs = self.target_config['ignore-vcs']
|
|
if not isinstance(ignore_vcs, bool):
|
|
raise TypeError(
|
|
'Field `tool.hatch.build.targets.{}.ignore-vcs` must be a boolean'.format(self.PLUGIN_NAME)
|
|
)
|
|
else:
|
|
ignore_vcs = self.build_config.get('ignore-vcs', False)
|
|
if not isinstance(ignore_vcs, bool):
|
|
raise TypeError('Field `tool.hatch.build.ignore-vcs` must be a boolean')
|
|
|
|
self.__ignore_vcs = ignore_vcs
|
|
|
|
return self.__ignore_vcs
|
|
|
|
@property
|
|
def reproducible(self):
|
|
"""
|
|
Whether or not the target should be built in a reproducible manner, defaulting to true.
|
|
"""
|
|
if self.__reproducible is None:
|
|
if 'reproducible' in self.target_config:
|
|
reproducible = self.target_config['reproducible']
|
|
if not isinstance(reproducible, bool):
|
|
raise TypeError(
|
|
'Field `tool.hatch.build.targets.{}.reproducible` must be a boolean'.format(self.PLUGIN_NAME)
|
|
)
|
|
else:
|
|
reproducible = self.build_config.get('reproducible', True)
|
|
if not isinstance(reproducible, bool):
|
|
raise TypeError('Field `tool.hatch.build.reproducible` must be a boolean')
|
|
|
|
self.__reproducible = reproducible
|
|
|
|
return self.__reproducible
|
|
|
|
@property
|
|
def dev_mode_dirs(self):
|
|
"""
|
|
Directories which must be added to Python's search path in [dev mode](../config/environment.md#dev-mode).
|
|
"""
|
|
if self.__dev_mode_dirs is None:
|
|
if 'dev-mode-dirs' in self.target_config:
|
|
dev_mode_dirs_config = self.target_config
|
|
dev_mode_dirs_location = 'tool.hatch.build.targets.{}.dev-mode-dirs'.format(self.PLUGIN_NAME)
|
|
else:
|
|
dev_mode_dirs_config = self.build_config
|
|
dev_mode_dirs_location = 'tool.hatch.build.dev-mode-dirs'
|
|
|
|
all_dev_mode_dirs = []
|
|
|
|
dev_mode_dirs = dev_mode_dirs_config.get('dev-mode-dirs', [])
|
|
if isinstance(dev_mode_dirs, str):
|
|
for dev_mode_dir in dev_mode_dirs.split(','):
|
|
if dev_mode_dir:
|
|
all_dev_mode_dirs.append(dev_mode_dir)
|
|
elif isinstance(dev_mode_dirs, list):
|
|
for i, dev_mode_dir in enumerate(dev_mode_dirs, 1):
|
|
if not isinstance(dev_mode_dir, str):
|
|
raise TypeError(
|
|
'Directory #{} in field `{}` must be a string'.format(i, dev_mode_dirs_location)
|
|
)
|
|
|
|
if dev_mode_dir:
|
|
all_dev_mode_dirs.append(dev_mode_dir)
|
|
else:
|
|
raise TypeError(
|
|
'Field `{}` must be a comma-separated string or an array of strings'.format(dev_mode_dirs_location)
|
|
)
|
|
|
|
self.__dev_mode_dirs = all_dev_mode_dirs
|
|
|
|
return self.__dev_mode_dirs
|
|
|
|
@property
|
|
def packages(self):
|
|
if self.__packages is None:
|
|
if 'packages' in self.target_config:
|
|
package_config = self.target_config
|
|
package_location = 'tool.hatch.build.targets.{}.packages'.format(self.PLUGIN_NAME)
|
|
else:
|
|
package_config = self.build_config
|
|
package_location = 'tool.hatch.build.packages'
|
|
|
|
all_packages = set()
|
|
|
|
packages = package_config.get('packages', [])
|
|
if isinstance(packages, str):
|
|
for package in packages.split(','):
|
|
if package:
|
|
all_packages.add(os.path.normpath(package).lstrip(os.path.sep))
|
|
elif isinstance(packages, list):
|
|
for i, package in enumerate(packages, 1):
|
|
if not isinstance(package, str):
|
|
raise TypeError('Package #{} in field `{}` must be a string'.format(i, package_location))
|
|
|
|
if package:
|
|
all_packages.add(os.path.normpath(package).lstrip(os.path.sep))
|
|
else:
|
|
raise TypeError(
|
|
'Field `{}` must be a comma-separated string or an array of strings'.format(package_location)
|
|
)
|
|
|
|
unique_packages = {}
|
|
package_data = []
|
|
|
|
for relative_path in sorted(all_packages):
|
|
source, package = os.path.split(relative_path)
|
|
if package in unique_packages:
|
|
raise ValueError(
|
|
'Package `{}` of field `{}` is already defined by path `{}`'.format(
|
|
package, package_location, unique_packages[package]
|
|
)
|
|
)
|
|
|
|
unique_packages[package] = relative_path
|
|
|
|
if source:
|
|
package_data.append((relative_path + os.path.sep, lambda path: path[len(source) + 1 :]))
|
|
else:
|
|
package_data.append((relative_path + os.path.sep, lambda path: path))
|
|
|
|
self.__packages = package_data
|
|
|
|
return self.__packages
|
|
|
|
@property
|
|
def versions(self):
|
|
if self.__versions is None:
|
|
# Used as an ordered set
|
|
all_versions = OrderedDict()
|
|
|
|
versions = self.target_config.get('versions', [])
|
|
if isinstance(versions, str):
|
|
for version in versions.split(','):
|
|
if version:
|
|
all_versions[version] = None
|
|
elif isinstance(versions, list):
|
|
for i, version in enumerate(versions, 1):
|
|
if not isinstance(version, str):
|
|
raise TypeError(
|
|
'Version #{} in field `tool.hatch.build.targets.{}.versions` must be a string'.format(
|
|
i, self.PLUGIN_NAME
|
|
)
|
|
)
|
|
|
|
if version:
|
|
all_versions[version] = None
|
|
else:
|
|
raise TypeError(
|
|
'Field `tool.hatch.build.targets.{}.versions` must be a comma-separated '
|
|
'string or an array of strings'.format(self.PLUGIN_NAME)
|
|
)
|
|
|
|
if not all_versions:
|
|
default_versions = self.get_default_versions()
|
|
for version in default_versions:
|
|
all_versions[version] = None
|
|
else:
|
|
unknown_versions = set(all_versions) - set(self.get_version_api())
|
|
if unknown_versions:
|
|
raise ValueError(
|
|
'Unknown versions in field `tool.hatch.build.targets.{}.versions`: {}'.format(
|
|
self.PLUGIN_NAME, ', '.join(map(str, sorted(unknown_versions)))
|
|
)
|
|
)
|
|
|
|
self.__versions = list(all_versions)
|
|
|
|
return self.__versions
|
|
|
|
@property
|
|
def dependencies(self):
|
|
if self.__dependencies is None:
|
|
# Used as an ordered set
|
|
dependencies = OrderedDict()
|
|
|
|
target_dependencies = self.target_config.get('dependencies', [])
|
|
if not isinstance(target_dependencies, list):
|
|
raise TypeError(
|
|
'Field `tool.hatch.build.targets.{}.dependencies` must be an array'.format(self.PLUGIN_NAME)
|
|
)
|
|
|
|
for i, dependency in enumerate(target_dependencies, 1):
|
|
if not isinstance(dependency, str):
|
|
raise TypeError(
|
|
'Dependency #{} of field `tool.hatch.build.targets.{}.dependencies` must be a string'.format(
|
|
i, self.PLUGIN_NAME
|
|
)
|
|
)
|
|
|
|
dependencies[dependency] = None
|
|
|
|
global_dependencies = self.build_config.get('dependencies', [])
|
|
if not isinstance(global_dependencies, list):
|
|
raise TypeError('Field `tool.hatch.build.dependencies` must be an array')
|
|
|
|
for i, dependency in enumerate(global_dependencies, 1):
|
|
if not isinstance(dependency, str):
|
|
raise TypeError(
|
|
'Dependency #{} of field `tool.hatch.build.dependencies` must be a string'.format(i)
|
|
)
|
|
|
|
dependencies[dependency] = None
|
|
|
|
for hook_name, config in self.hook_config.items():
|
|
hook_dependencies = config.get('dependencies', [])
|
|
if not isinstance(hook_dependencies, list):
|
|
raise TypeError('Option `dependencies` of build hook `{}` must be an array'.format(hook_name))
|
|
|
|
for i, dependency in enumerate(hook_dependencies, 1):
|
|
if not isinstance(dependency, str):
|
|
raise TypeError(
|
|
'Dependency #{} of option `dependencies` of build hook `{}` must be a string'.format(
|
|
i, hook_name
|
|
)
|
|
)
|
|
|
|
dependencies[dependency] = None
|
|
|
|
self.__dependencies = list(dependencies)
|
|
|
|
return self.__dependencies
|
|
|
|
@property
|
|
def project_id(self):
|
|
if self.__project_id is None:
|
|
self.__project_id = '{}-{}'.format(
|
|
# https://discuss.python.org/t/clarify-naming-of-dist-info-directories/5565
|
|
self.normalize_file_name_component(self.metadata.core.name),
|
|
self.metadata.version,
|
|
)
|
|
|
|
return self.__project_id
|
|
|
|
def get_build_hooks(self, directory):
|
|
hook_config = self.hook_config
|
|
|
|
configured_build_hooks = OrderedDict()
|
|
for hook_name, config in hook_config.items():
|
|
build_hook = self.plugin_manager.build_hook.get(hook_name)
|
|
if build_hook is None:
|
|
raise ValueError('Unknown build hook: {}'.format(hook_name))
|
|
|
|
configured_build_hooks[hook_name] = build_hook(self.root, config, directory, self.PLUGIN_NAME, self.app)
|
|
|
|
return configured_build_hooks
|
|
|
|
def get_distribution_path(self, relative_path):
|
|
# src/foo/bar.py -> foo/bar.py
|
|
for package_relative_path, formatter in self.packages:
|
|
if relative_path.startswith(package_relative_path):
|
|
return formatter(relative_path)
|
|
|
|
return relative_path
|
|
|
|
def load_vcs_ignore_patterns(self):
|
|
# https://git-scm.com/docs/gitignore#_pattern_format
|
|
default_exclusion_file = locate_file(self.root, '.gitignore')
|
|
if default_exclusion_file is None:
|
|
return []
|
|
|
|
with open(default_exclusion_file, 'r', encoding='utf-8') as f:
|
|
return f.readlines()
|
|
|
|
def default_include_patterns(self):
|
|
return []
|
|
|
|
def default_exclude_patterns(self):
|
|
return []
|
|
|
|
def default_global_exclude_patterns(self):
|
|
return ['.git']
|
|
|
|
def ignore_directory(self, directory):
|
|
# A hack used by WheelBuilder to ignore any test directories when no inclusion patterns are provided.
|
|
return False
|
|
|
|
def ignore_files(self, files):
|
|
# A hack used by WheelBuilder to ignore any non-package directory when no inclusion patterns are provided.
|
|
return False
|
|
|
|
def get_version_api(self):
|
|
"""
|
|
:material-align-horizontal-left: **REQUIRED** :material-align-horizontal-right:
|
|
|
|
A mapping of `str` versions to a callable that is used for building.
|
|
Each callable must have the following signature:
|
|
|
|
```python
|
|
def ...(build_dir: str, build_data: dict) -> str:
|
|
```
|
|
|
|
The return value must be the absolute path to the built artifact.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def get_default_versions(self):
|
|
"""
|
|
A list of versions to build when users do not specify any, defaulting to all versions.
|
|
"""
|
|
return list(self.get_version_api())
|
|
|
|
def get_default_build_data(self):
|
|
"""
|
|
A mapping that can be modified by [build hooks](build-hook.md) to influence the behavior of builds.
|
|
"""
|
|
return {}
|
|
|
|
def clean(self, directory, versions):
|
|
"""
|
|
Called before builds if the `-c`/`--clean` flag was passed to the
|
|
[`build`](../cli/reference.md#hatch-build) command.
|
|
"""
|
|
|
|
@contextmanager
|
|
def configure_with_build_data(self, build_data):
|
|
try:
|
|
# Include anything the hooks indicate
|
|
build_artifacts = build_data['artifacts']
|
|
if build_artifacts:
|
|
self.build_artifact_spec = pathspec.PathSpec.from_lines(
|
|
pathspec.patterns.GitWildMatchPattern, build_artifacts
|
|
)
|
|
|
|
yield
|
|
finally:
|
|
self.build_artifact_spec = None
|
|
|
|
def _normalize_build_directory(self, build_directory):
|
|
if not os.path.isabs(build_directory):
|
|
build_directory = os.path.join(self.root, build_directory)
|
|
|
|
return os.path.normpath(build_directory)
|
|
|
|
@staticmethod
|
|
def normalize_file_name_component(file_name):
|
|
"""
|
|
https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode
|
|
"""
|
|
return re.sub(r'[^\w\d.]+', '_', file_name, re.UNICODE)
|
|
|
|
|
|
def _env_var_enabled(env_var, default=False):
|
|
if env_var in os.environ:
|
|
return os.environ[env_var] in ('1', 'true')
|
|
else:
|
|
return default
|