Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
- fix #1099 use file modification times for dirty working directory timestamps instead of current time
- fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields
- add `scm` parameter support to `get_version()` function for nested SCM configuration
- fix #987: expand documentation on git archival files and add cli tools for good defaults

### Changed

- add `pip` to test optional dependencies for improved uv venv compatibility
Expand Down
116 changes: 110 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,15 +376,41 @@ accordingly.

### Git archives

Git archives are supported, but a few changes to your repository are required.
Git archives are supported, but require specific setup and understanding of how they work with package building.

Ensure the content of the following files:
#### Overview

When you create a `.git_archival.txt` file in your repository, it enables setuptools-scm to extract version information from git archives (e.g., GitHub's source downloads). However, this file contains template placeholders that must be expanded by `git archive` - they won't work when building directly from your working directory.

#### Setting up git archival support

You can generate a `.git_archival.txt` file using the setuptools-scm CLI:

```commandline
# Generate a stable archival file (recommended for releases)
$ python -m setuptools_scm create-archival-file --stable

# Generate a full archival file with all metadata (use with caution)
$ python -m setuptools_scm create-archival-file --full
```

Alternatively, you can create the file manually:

**Stable version (recommended):**
```{ .text file=".git_archival.txt"}
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
```

**Full version (with branch information - can cause instability):**
```{ .text file=".git_archival.txt"}
# WARNING: Including ref-names can make archive checksums unstable
# after commits are added post-release. Use only if describe-name is insufficient.
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
ref-names: $Format:%D$
```

Feel free to alter the `match` field in `describe-name` to match your project's
Expand All @@ -402,14 +428,92 @@ tagging style.
.git_archival.txt export-subst
```

Finally, don't forget to commit the two files:
Finally, commit both files:
```commandline
$ git add .git_archival.txt .gitattributes && git commit -m "add export config"
$ git add .git_archival.txt .gitattributes && git commit -m "add git archive support"
```

#### Understanding the warnings

If you see warnings like these when building your package:

```
UserWarning: git archive did not support describe output
UserWarning: unprocessed git archival found (no export subst applied)
```

This typically happens when:

1. **Building from working directory**: You're running `python -m build` directly in your repository
2. **Sdist extraction**: A build tool extracts your sdist to build wheels, but the extracted directory isn't a git repository

#### Recommended build workflows

**For development builds:**
Exclude `.git_archival.txt` from your package to avoid warnings:

```{ .text file="MANIFEST.in"}
# Exclude archival file from development builds
exclude .git_archival.txt
```

**For release builds from archives:**
Build from an actual git archive to ensure proper template expansion:

```commandline
# Create archive from a specific tag/commit
$ git archive --output=../source_archive.tar v1.2.3
$ cd ..
$ tar -xf source_archive.tar
$ cd extracted_directory/
$ python -m build .
```

**For automated releases:**
Many CI systems and package repositories (like GitHub Actions) automatically handle this correctly when building from git archives.

#### Integration with package managers

**MANIFEST.in exclusions:**
```{ .text file="MANIFEST.in"}
# Exclude development files from packages
exclude .git_archival.txt
exclude .gitattributes
```


```{ .text file=".gitattributes"}
# Archive configuration
.git_archival.txt export-subst
.gitignore export-ignore
```

#### Troubleshooting

**Problem: "unprocessed git archival found" warnings**
- ✅ **Solution**: Add `exclude .git_archival.txt` to `MANIFEST.in` for development builds
- ✅ **Alternative**: Build from actual git archives for releases

**Problem: "git archive did not support describe output" warnings**
- ℹ️ **Information**: This is expected when `.git_archival.txt` contains unexpanded templates
- ✅ **Solution**: Same as above - exclude file or build from git archives

**Problem: Version detection fails in git archives**
- ✅ **Check**: Is `.gitattributes` configured with `export-subst`?
- ✅ **Check**: Are you building from a properly created git archive?
- ✅ **Check**: Does your git hosting provider support archive template expansion?

!!! warning "Branch Names and Archive Stability"

Including `ref-names: $Format:%D$` in your `.git_archival.txt` can make archive checksums change when new commits are added to branches referenced in the archive. This primarily affects GitHub's automatic source archives. Use the stable format (without `ref-names`) unless you specifically need branch information and understand the stability implications.

!!! note "Version Files"

Note that if you are creating a `_version.py` file, note that it should not
be kept in version control. It's strongly recommended to be put into gitignore.
If you are creating a `_version.py` file, it should not be kept in version control. Add it to `.gitignore`:
```
# Generated version file
src/mypackage/_version.py
```

[git-archive-issue]: https://github.com/pypa/setuptools-scm/issues/806

Expand Down
102 changes: 102 additions & 0 deletions src/setuptools_scm/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import sys

from pathlib import Path
from typing import Any

from setuptools_scm import Configuration
Expand Down Expand Up @@ -106,6 +107,28 @@ def _get_cli_opts(args: list[str] | None) -> argparse.Namespace:
# We avoid `metavar` to prevent printing repetitive information
desc = "List information about the package, e.g. included files"
sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc)

# Add create-archival-file subcommand
archival_desc = "Create .git_archival.txt file for git archive support"
archival_parser = sub.add_parser(
"create-archival-file",
help=archival_desc[0].lower() + archival_desc[1:],
description=archival_desc,
)
archival_group = archival_parser.add_mutually_exclusive_group(required=True)
archival_group.add_argument(
"--stable",
action="store_true",
help="create stable archival file (recommended, no branch names)",
)
archival_group.add_argument(
"--full",
action="store_true",
help="create full archival file with branch information (can cause instability)",
)
archival_parser.add_argument(
"--force", action="store_true", help="overwrite existing .git_archival.txt file"
)
return parser.parse_args(args)


Expand All @@ -116,6 +139,9 @@ def command(opts: argparse.Namespace, version: str, config: Configuration) -> in
if opts.command == "ls":
opts.query = ["files"]

if opts.command == "create-archival-file":
return _create_archival_file(opts, config)

if opts.query == []:
opts.no_version = True
sys.stderr.write("Available queries:\n\n")
Expand Down Expand Up @@ -187,3 +213,79 @@ def _find_pyproject(parent: str) -> str:
return os.path.abspath(
"pyproject.toml"
) # use default name to trigger the default errors


def _create_archival_file(opts: argparse.Namespace, config: Configuration) -> int:
"""Create .git_archival.txt file with appropriate content."""
archival_path = Path(config.root, ".git_archival.txt")

# Check if file exists and force flag
if archival_path.exists() and not opts.force:
print(
f"Error: {archival_path} already exists. Use --force to overwrite.",
file=sys.stderr,
)
return 1

if opts.stable:
content = _get_stable_archival_content()
print("Creating stable .git_archival.txt (recommended for releases)")
elif opts.full:
content = _get_full_archival_content()
print("Creating full .git_archival.txt with branch information")
print("WARNING: This can cause archive checksums to be unstable!")

try:
archival_path.write_text(content, encoding="utf-8")
print(f"Created: {archival_path}")

gitattributes_path = Path(config.root, ".gitattributes")
needs_gitattributes = True

if gitattributes_path.exists():
# TODO: more nuanced check later
gitattributes_content = gitattributes_path.read_text("utf-8")
if (
".git_archival.txt" in gitattributes_content
and "export-subst" in gitattributes_content
):
needs_gitattributes = False

if needs_gitattributes:
print("\nNext steps:")
print("1. Add this line to .gitattributes:")
print(" .git_archival.txt export-subst")
print("2. Commit both files:")
print(" git add .git_archival.txt .gitattributes")
print(" git commit -m 'add git archive support'")
else:
print("\nNext step:")
print("Commit the archival file:")
print(" git add .git_archival.txt")
print(" git commit -m 'update git archival file'")

return 0
except OSError as e:
print(f"Error: Could not create {archival_path}: {e}", file=sys.stderr)
return 1


def _get_stable_archival_content() -> str:
"""Generate stable archival file content (no branch names)."""
return """\
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
"""


def _get_full_archival_content() -> str:
"""Generate full archival file content with branch information."""
return """\
# WARNING: Including ref-names can make archive checksums unstable
# after commits are added post-release. Use only if describe-name is insufficient.
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
ref-names: $Format:%D$
"""
Loading
Loading