1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-07 23:35:16 +00:00
terraform-playground/pymage/internal/lock/lock.go
Cursor Agent 7290806232
wheel: strip extras from console-script entry points; lock: strip inline comments from first '#'
- console-script targets like 'pkg.mod:main [extra]' no longer leak the
  '[extra]' suffix into the generated launcher; module-less entry points are
  skipped instead of producing invalid Python
- stripComment now strips from the first '#' (not only ' #') and trims trailing
  whitespace, matching its docstring
- adds tests demonstrating both behaviors

Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
2026-06-10 15:25:47 +00:00

159 lines
5 KiB
Go

// Package lock parses fully-pinned, hashed requirements files (the output of
// `pip-compile` / `uv pip compile --generate-hashes`).
//
// We deliberately do not resolve dependencies ourselves; we consume a lock that
// already pins every distribution to an exact version with one or more sha256
// hashes. The hashes are what make a build reproducible and let us key the
// layer cache by wheel content.
package lock
import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strings"
)
// Requirement is a single pinned distribution.
type Requirement struct {
// Name is the project name as written (normalize with NormalizeName for
// comparisons).
Name string
// Version is the exact pinned version (from `==`).
Version string
// Hashes are the allowed sha256 hex digests (without the "sha256:" prefix).
Hashes []string
}
var (
// pinRE matches "name[extras]==version". Extras are accepted (and ignored
// for matching, since a resolved lock pins their dependencies separately);
// environment markers after ";" are also ignored.
pinRE = regexp.MustCompile(`^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*==\s*([^\s;]+)`)
hashRE = regexp.MustCompile(`--hash=sha256:([0-9a-fA-F]{64})`)
normRE = regexp.MustCompile(`[-_.]+`)
// dependencyOptions reference further requirements rather than configuring
// the resolver. Silently skipping them could omit dependencies, so we
// reject them: a lock fed to pymage must be fully inlined and pinned.
dependencyOptions = []string{"-e", "--editable", "-r", "--requirement", "-c", "--constraint"}
)
// NormalizeName implements PEP 503 name normalization.
func NormalizeName(name string) string {
return strings.ToLower(normRE.ReplaceAllString(name, "-"))
}
// ParseFile reads and parses a requirements file from disk.
func ParseFile(path string) ([]Requirement, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
return Parse(f)
}
// Parse reads pinned requirements from r. Logical lines may span multiple
// physical lines using a trailing backslash, which is how `--hash` entries are
// typically formatted.
func Parse(r io.Reader) ([]Requirement, error) {
var reqs []Requirement
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
var logical strings.Builder
flush := func() error {
line := logical.String()
logical.Reset()
stripped := stripComment(line)
if strings.TrimSpace(stripped) == "" {
return nil
}
// Configuration options (e.g. --index-url) aren't pins and are skipped,
// but options that pull in other requirements must not be silently
// ignored.
if isOption(stripped) {
return optionError(stripped)
}
m := pinRE.FindStringSubmatch(stripped)
if m == nil {
// We only accept fully-pinned "name==version" requirements. Refuse
// (rather than silently drop) anything else — unpinned constraints,
// URL/VCS direct references, `-e` editables — so a lock can never
// quietly omit a dependency from the image.
return fmt.Errorf("lock: unsupported requirement line %q (expected name==version)", strings.TrimSpace(stripped))
}
req := Requirement{Name: m[1], Version: m[2]}
for _, h := range hashRE.FindAllStringSubmatch(stripped, -1) {
req.Hashes = append(req.Hashes, strings.ToLower(h[1]))
}
reqs = append(reqs, req)
return nil
}
for sc.Scan() {
line := sc.Text()
trimmed := strings.TrimRight(line, " \t")
if strings.HasSuffix(trimmed, "\\") {
logical.WriteString(strings.TrimSuffix(trimmed, "\\"))
logical.WriteString(" ")
continue
}
logical.WriteString(line)
if err := flush(); err != nil {
return nil, err
}
}
if err := sc.Err(); err != nil {
return nil, err
}
if logical.Len() > 0 {
if err := flush(); err != nil {
return nil, err
}
}
return reqs, nil
}
func stripComment(s string) string {
// A '#' starts a comment. Requirements files don't support escaping '#',
// and no valid pinned/hashed line contains one, so we strip from the first
// '#' and drop any trailing whitespace it leaves behind.
if i := strings.IndexByte(s, '#'); i >= 0 {
s = s[:i]
}
return strings.TrimRight(s, " \t")
}
func isOption(s string) bool {
t := strings.TrimSpace(s)
return strings.HasPrefix(t, "-")
}
// optionError returns an error for dependency-bearing options (which we cannot
// honor from a local wheelhouse), and nil for benign configuration options.
func optionError(s string) error {
t := strings.TrimSpace(s)
for _, opt := range dependencyOptions {
if t == opt || strings.HasPrefix(t, opt+" ") || strings.HasPrefix(t, opt+"=") {
return fmt.Errorf("lock: unsupported option %q; referenced requirements must be inlined and pinned", t)
}
}
return nil
}
// Validate ensures every requirement is pinned and (optionally) hashed.
func Validate(reqs []Requirement, requireHashes bool) error {
for _, r := range reqs {
if r.Version == "" {
return fmt.Errorf("lock: requirement %q is not pinned with ==", r.Name)
}
if requireHashes && len(r.Hashes) == 0 {
return fmt.Errorf("lock: requirement %q has no --hash entries", r.Name)
}
}
return nil
}