Compare commits
19
Commits
c1f8893576
...
0235ec6457
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0235ec6457 | ||
|
|
fa399f64b2 | ||
|
|
e6f2012835 | ||
|
|
ffac4d6b57 | ||
|
|
04a572cd77 | ||
|
|
fc0d2f247e | ||
|
|
ccd2e37385 | ||
|
|
52d5ad064f | ||
|
|
b8b2be5acf | ||
|
|
dd1c70c91a | ||
|
|
12407c8eac | ||
|
|
7601524b7c | ||
|
|
7af767898e | ||
|
|
ead97e1213 | ||
|
|
6e5ecd2f45 | ||
|
|
69f3c3954e | ||
|
|
c106ebe315 | ||
|
|
941f91decf | ||
|
|
b489db2728 |
@@ -3,13 +3,45 @@
|
|||||||
## update and maintain such data in one unique place
|
## update and maintain such data in one unique place
|
||||||
## The goal is to have the minimal possible set of data necessary
|
## The goal is to have the minimal possible set of data necessary
|
||||||
## to grab the actual data. For example we don't want to store every Ubuntu
|
## to grab the actual data. For example we don't want to store every Ubuntu
|
||||||
## or Debian series, but rather an URL where we can properly access that data.
|
## or Debian series, but rather pointers to where that data lives: each dist
|
||||||
dist_info:
|
## entry below carries its series sources (the local distro-info CSV, with
|
||||||
local: /usr/share/distro-info/{dist}
|
## the network URL as fallback).
|
||||||
network: https://salsa.debian.org/debian/distro-info-data/-/raw/main/
|
##
|
||||||
|
## Per-dist keys beyond the series pointers:
|
||||||
|
## mirrors: the archive mirrors, each a URL serving a set of
|
||||||
|
## architectures: `primary` (the main archive, whose url
|
||||||
|
## doubles as the dist's base URL) and, where they exist,
|
||||||
|
## the others (`ports`). `security_url` is the sibling
|
||||||
|
## host serving the -security pocket for the same arches
|
||||||
|
## (ports mirrors serve their own security); `archs` is
|
||||||
|
## an explicit list, or the `all` sentinel when one
|
||||||
|
## mirror serves every architecture (Debian's case — an
|
||||||
|
## exhaustive list would rot each time an arch is added).
|
||||||
|
## Host matching treats a URI as official when its host
|
||||||
|
## equals a mirror host or is a subdomain of it, so the
|
||||||
|
## country mirrors (fr.archive.ubuntu.com) count too.
|
||||||
|
## components: the archive components (main, universe, contrib, ...)
|
||||||
|
## a cross-build environment enables on official sources.
|
||||||
|
## Live archive operations keep resolving components from
|
||||||
|
## Release files; this is the offline default.
|
||||||
|
## cross_pockets: the pockets a cross-build environment enables for a
|
||||||
|
## series (`<series>-updates`, ...). Deliberately not the
|
||||||
|
## `pockets` key: that one is the *search order* of pull,
|
||||||
|
## where backports must not fold in.
|
||||||
|
## build_profiles: the vendor's default DEB_BUILD_PROFILES (Ubuntu
|
||||||
|
## activates derivative.ubuntu noudeb, Debian none),
|
||||||
|
## mirroring what Dpkg::BuildProfiles resolves when the
|
||||||
|
## variable is unset.
|
||||||
dist:
|
dist:
|
||||||
debian:
|
debian:
|
||||||
base_url: https://deb.debian.org/debian
|
mirrors:
|
||||||
|
primary:
|
||||||
|
url: https://deb.debian.org/debian
|
||||||
|
# One mirror serves every architecture.
|
||||||
|
archs: all
|
||||||
|
components: [main, contrib, non-free, non-free-firmware]
|
||||||
|
cross_pockets: [updates, backports, security]
|
||||||
|
build_profiles: []
|
||||||
archive_keyring: https://ftp-master.debian.org/keys/archive-key-{series_num}.asc
|
archive_keyring: https://ftp-master.debian.org/keys/archive-key-{series_num}.asc
|
||||||
pockets:
|
pockets:
|
||||||
- updates
|
- updates
|
||||||
@@ -83,7 +115,20 @@ dist:
|
|||||||
local: /usr/share/distro-info/debian.csv
|
local: /usr/share/distro-info/debian.csv
|
||||||
network: https://salsa.debian.org/debian/distro-info-data/-/raw/main/debian.csv
|
network: https://salsa.debian.org/debian/distro-info-data/-/raw/main/debian.csv
|
||||||
ubuntu:
|
ubuntu:
|
||||||
base_url: https://archive.ubuntu.com/ubuntu
|
mirrors:
|
||||||
|
primary:
|
||||||
|
url: https://archive.ubuntu.com/ubuntu
|
||||||
|
# Sibling host serving the -security pocket for the same arches.
|
||||||
|
security_url: http://security.ubuntu.com/ubuntu
|
||||||
|
archs: [amd64, i386]
|
||||||
|
ports:
|
||||||
|
# Everything else lives on the ports archive, which also serves
|
||||||
|
# its own -security pocket (no security_url needed).
|
||||||
|
url: http://ports.ubuntu.com/ubuntu-ports
|
||||||
|
archs: [armhf, arm64, ppc64el, riscv64, s390x]
|
||||||
|
components: [main, restricted, universe, multiverse]
|
||||||
|
cross_pockets: [updates, backports, security]
|
||||||
|
build_profiles: [derivative.ubuntu, noudeb]
|
||||||
archive_keyring: https://archive.ubuntu.com/ubuntu/project/ubuntu-archive-keyring.gpg
|
archive_keyring: https://archive.ubuntu.com/ubuntu/project/ubuntu-archive-keyring.gpg
|
||||||
pockets:
|
pockets:
|
||||||
- updates
|
- updates
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## Forge hosts recognized by `pkh new` origin detection, with the
|
||||||
|
## release-tarball URL templates of each: `Forge::parse`
|
||||||
|
## (src/new/origin.rs) matches a git remote's host against the map keys,
|
||||||
|
## and the tarball download substitutes {owner}, {repo} and {tag} into the
|
||||||
|
## templates. Like host_keys.yml, this file exists so that static
|
||||||
|
## endpoints are data: adding a forge is a YAML entry, not a code change
|
||||||
|
## (self-hosted instances are deliberately absent — the download URL
|
||||||
|
## shapes differ per instance).
|
||||||
|
##
|
||||||
|
## tarball_templates are tried sequentially in file order, best candidate
|
||||||
|
## first (GitHub prefers the codeload direct link: no redirect).
|
||||||
|
## `kind` documents the forge family the URL shapes belong to; the
|
||||||
|
## templates fully describe the URLs, so nothing branches on it (yet) —
|
||||||
|
## but it must be one of the known kinds, enforced at load time.
|
||||||
|
##
|
||||||
|
## Where the values come from: each forge's release-archive download URL
|
||||||
|
## shapes, verified against the live forges —
|
||||||
|
## github: codeload.github.com/<owner>/<repo>/tar.gz/refs/tags/<tag>
|
||||||
|
## and github.com/<owner>/<repo>/archive/refs/tags/<tag>.tar.gz
|
||||||
|
## gitlab: gitlab.com/<owner>/<repo>/-/archive/<tag>/<repo>-<tag>.tar.gz
|
||||||
|
|
||||||
|
forges:
|
||||||
|
github.com:
|
||||||
|
kind: github
|
||||||
|
tarball_templates:
|
||||||
|
- https://codeload.github.com/{owner}/{repo}/tar.gz/refs/tags/{tag}
|
||||||
|
- https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.tar.gz
|
||||||
|
gitlab.com:
|
||||||
|
kind: gitlab
|
||||||
|
tarball_templates:
|
||||||
|
- https://gitlab.com/{owner}/{repo}/-/archive/{tag}/{repo}-{tag}.tar.gz
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## Keyserver lookup endpoint used to fetch PPA signing keys.
|
||||||
|
## Like host_keys.yml, this file exists so that a static endpoint is data,
|
||||||
|
## updatable in one reviewable place, instead of hardcoded in the source —
|
||||||
|
## the URL was previously duplicated in the apt keyring and release
|
||||||
|
## modules. Sparse on purpose: it grows if keyserver pools or alternates
|
||||||
|
## ever need to be tried.
|
||||||
|
##
|
||||||
|
## The template carries its variable part as a {fingerprint} placeholder,
|
||||||
|
## substituted by the accessor of src/apt/keyring.rs with plain string
|
||||||
|
## replacement.
|
||||||
|
##
|
||||||
|
## Where the value comes from: keyserver.ubuntu.com, Ubuntu's OpenPKS
|
||||||
|
## (formerly SKS) keyserver; op=get with search=0x<fingerprint> is the
|
||||||
|
## documented machine interface fetching one key by fingerprint
|
||||||
|
## (https://keyserver.ubuntu.com).
|
||||||
|
|
||||||
|
lookup_template: "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x{fingerprint}"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## Launchpad service endpoints: the REST API, the PPA SFTP upload queue,
|
||||||
|
## the PPA package-content host and the Ubuntu source-package git web UI.
|
||||||
|
## Like host_keys.yml, this file exists so that static endpoints are data,
|
||||||
|
## updatable in one reviewable place, instead of hardcoded in the source —
|
||||||
|
## the API and content URLs were previously triplicated across modules.
|
||||||
|
##
|
||||||
|
## Templates carry their variable parts as {name} placeholders ({owner},
|
||||||
|
## {ppa}, {package}), substituted by the accessors of src/launchpad.rs
|
||||||
|
## with plain string replacement.
|
||||||
|
##
|
||||||
|
## Where the values come from:
|
||||||
|
## api_base: the Launchpad REST API root (https://launchpad.net/docs/api/)
|
||||||
|
## ssh_*: the PPA upload queue, as expanded by dput-ng's
|
||||||
|
## ppa:user/ppa profile (ppa.launchpad.net:22, incoming
|
||||||
|
## ~<user>/<ppa>)
|
||||||
|
## content_host_template: ppa.launchpadcontent.net serves PPA apt
|
||||||
|
## repositories since the 2022 move off ppa.launchpad.net
|
||||||
|
## git_web_template: Launchpad's CGit mirrors of Ubuntu source packages
|
||||||
|
## (git.launchpad.net/ubuntu/+source/<package>)
|
||||||
|
|
||||||
|
api_base: https://api.launchpad.net/1.0
|
||||||
|
ssh_host: ppa.launchpad.net
|
||||||
|
ssh_port: 22
|
||||||
|
incoming_template: "~{owner}/{ppa}"
|
||||||
|
content_host_template: https://ppa.launchpadcontent.net/{owner}/{ppa}/ubuntu
|
||||||
|
git_web_template: https://git.launchpad.net/ubuntu/+source/{package}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
## License knowledge of `pkh new`, in one place: the wizard menu labels
|
||||||
|
## (src/new/questions.rs), the spellings accepted by License::parse and the
|
||||||
|
## SPDX URL template (src/new/options.rs), and the license-file sniffing
|
||||||
|
## inputs (src/new/detect.rs) all read this table, so the three lists —
|
||||||
|
## previously kept in sync by comments only — cannot drift apart anymore.
|
||||||
|
## Adding or changing a curated license is one entry below.
|
||||||
|
##
|
||||||
|
## Keys:
|
||||||
|
## id: SPDX identifier: written to debian/copyright, returned
|
||||||
|
## by the license sniff and substituted into
|
||||||
|
## license_url_template (minus a trailing '+' of the
|
||||||
|
## "or later" spellings)
|
||||||
|
## menu: label offered by the wizard license question (the
|
||||||
|
## free-text "Other (enter a SPDX identifier)" entry
|
||||||
|
## stays in Rust — it is UX, not data)
|
||||||
|
## spellings: inputs accepted by License::parse, matched
|
||||||
|
## case-insensitively; each entry must accept its own id
|
||||||
|
## (a consistency test in options.rs locks ids, spellings
|
||||||
|
## and the License enum together)
|
||||||
|
## detect_markers: marker sets driving the LICENSE/COPYING text sniff of
|
||||||
|
## detect.rs. A set matches when every marker of `all`
|
||||||
|
## occurs in the lowercased license text and none of
|
||||||
|
## `unless` does; an entry matches when any of its sets
|
||||||
|
## does. The sets are written to be mutually exclusive:
|
||||||
|
## the `unless` markers keep multi-license texts on the
|
||||||
|
## entry carrying the stronger reference (a MIT-named
|
||||||
|
## file also quoting the GPL or the Apache license is a
|
||||||
|
## GPL/Apache file) and keep GPL sets off LGPL texts,
|
||||||
|
## whose name contains theirs. The entry order below
|
||||||
|
## (menu order) therefore only breaks ties.
|
||||||
|
##
|
||||||
|
## detect_files (top level): the candidate license file names the sniff
|
||||||
|
## reads, in preference order, shared by every license (the case-variant
|
||||||
|
## directory scan around them stays in Rust).
|
||||||
|
##
|
||||||
|
## license_url_template: the SPDX license page URL, with {id} substituted
|
||||||
|
## for the debian/copyright reference paragraph.
|
||||||
|
##
|
||||||
|
## The behavioral lock for the markers is the LICENSE_TEXTS test table in
|
||||||
|
## src/new/detect.rs: a bad marker edit fails those tests, not packages.
|
||||||
|
|
||||||
|
license_url_template: https://spdx.org/licenses/{id}.html
|
||||||
|
|
||||||
|
detect_files:
|
||||||
|
- LICENSE
|
||||||
|
- LICENSE.md
|
||||||
|
- LICENSE.txt
|
||||||
|
- COPYING
|
||||||
|
- COPYING.txt
|
||||||
|
|
||||||
|
# Entries in wizard-menu order.
|
||||||
|
licenses:
|
||||||
|
- id: MIT
|
||||||
|
menu: MIT
|
||||||
|
spellings: [MIT]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- mit license
|
||||||
|
unless:
|
||||||
|
- apache license
|
||||||
|
- general public license
|
||||||
|
- all:
|
||||||
|
- permission is hereby granted, free of charge
|
||||||
|
unless:
|
||||||
|
- apache license
|
||||||
|
- general public license
|
||||||
|
- id: Apache-2.0
|
||||||
|
menu: Apache-2.0
|
||||||
|
spellings: [Apache-2.0]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- apache license
|
||||||
|
- version 2
|
||||||
|
- id: GPL-2.0+
|
||||||
|
menu: GPL-2.0+
|
||||||
|
spellings: [GPL-2.0+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- general public license
|
||||||
|
unless:
|
||||||
|
- version 3
|
||||||
|
- lesser general public license
|
||||||
|
- id: GPL-3.0+
|
||||||
|
menu: GPL-3.0+
|
||||||
|
spellings: [GPL-3.0+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- general public license
|
||||||
|
- version 3
|
||||||
|
unless:
|
||||||
|
- lesser general public license
|
||||||
|
- id: LGPL-2.1+
|
||||||
|
menu: LGPL-2.1+
|
||||||
|
spellings: [LGPL-2.1+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- lesser general public license
|
||||||
|
unless:
|
||||||
|
- version 3
|
||||||
|
- all:
|
||||||
|
- lesser general public license
|
||||||
|
- version 2.1
|
||||||
|
- id: LGPL-3.0+
|
||||||
|
menu: LGPL-3.0+
|
||||||
|
spellings: [LGPL-3.0+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- lesser general public license
|
||||||
|
- version 3
|
||||||
|
unless:
|
||||||
|
- version 2.1
|
||||||
|
- id: BSD-2-Clause
|
||||||
|
menu: BSD-2-Clause
|
||||||
|
spellings: [BSD-2-Clause]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- redistribution and use in source and binary forms
|
||||||
|
unless:
|
||||||
|
- endorse or promote
|
||||||
|
- isc license
|
||||||
|
- permission to use, copy, modify, and/or distribute this software
|
||||||
|
- id: BSD-3-Clause
|
||||||
|
menu: BSD-3-Clause
|
||||||
|
spellings: [BSD-3-Clause]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- redistribution and use in source and binary forms
|
||||||
|
- endorse or promote
|
||||||
|
unless:
|
||||||
|
- isc license
|
||||||
|
- permission to use, copy, modify, and/or distribute this software
|
||||||
|
- id: ISC
|
||||||
|
menu: ISC
|
||||||
|
spellings: [ISC]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- isc license
|
||||||
|
- all:
|
||||||
|
- permission to use, copy, modify, and/or distribute this software
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bin_PROGRAMS = {command}
|
||||||
|
{command}_SOURCES = hello.c
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
AC_INIT([{name}], [{upstream_version}])
|
||||||
|
AM_INIT_AUTOMAKE([foreign])
|
||||||
|
AC_PROG_CC
|
||||||
|
AC_CONFIG_FILES([Makefile])
|
||||||
|
AC_OUTPUT
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Placeholder for {name}, generated by `pkh new`. */
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("Hello from {command}!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## The `autotools` template: a C project with a configure.ac built through
|
||||||
|
## debhelper's auto-detection (dh runs autoreconf itself when it finds
|
||||||
|
## configure.ac, debhelper >= 10 — no override needed). The skeleton bodies
|
||||||
|
## below are static data (the first source build runs `autoreconf`,
|
||||||
|
## integrated in the dh sequence, so no generated configure script is
|
||||||
|
## committed); the logic half — the AC_INIT probe and the GNU-gettext
|
||||||
|
## detection (appended to Build-Depends) — lives in
|
||||||
|
## src/new/templates/autotools.rs. hello.c.tpl duplicates the shared C
|
||||||
|
## skeleton of the other C/C++ template directories (see
|
||||||
|
## meson/manifest.yml for why).
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: autotools
|
||||||
|
label: C/C++ (Autotools)
|
||||||
|
detect:
|
||||||
|
files: [configure.ac]
|
||||||
|
build_depends:
|
||||||
|
- autoconf
|
||||||
|
- automake
|
||||||
|
- libtool
|
||||||
|
architecture: any
|
||||||
|
rules_dh_line: "dh $@"
|
||||||
|
files:
|
||||||
|
- path: configure.ac
|
||||||
|
template: configure.ac.tpl
|
||||||
|
- path: Makefile.am
|
||||||
|
template: Makefile.am.tpl
|
||||||
|
- path: hello.c
|
||||||
|
template: hello.c.tpl
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project({name} VERSION {upstream_version})
|
||||||
|
|
||||||
|
add_executable({command} hello.c)
|
||||||
|
install(TARGETS {command} RUNTIME DESTINATION bin)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Placeholder for {name}, generated by `pkh new`. */
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("Hello from {command}!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
## The `cmake` template: a C/C++ project built with CMake through the
|
||||||
|
## debhelper cmake buildsystem. The skeleton bodies below are static data;
|
||||||
|
## the logic half — the project() probe and the wizard's pkg-config
|
||||||
|
## opt-in (appended to Build-Depends) — lives in src/new/templates/cmake.rs.
|
||||||
|
## hello.c.tpl duplicates the shared C skeleton of the other C/C++
|
||||||
|
## template directories (see meson/manifest.yml for why).
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: cmake
|
||||||
|
label: C/C++ (CMake)
|
||||||
|
detect:
|
||||||
|
files: [CMakeLists.txt]
|
||||||
|
build_depends:
|
||||||
|
- cmake
|
||||||
|
architecture: any
|
||||||
|
rules_dh_line: "dh $@ --buildsystem=cmake"
|
||||||
|
files:
|
||||||
|
- path: CMakeLists.txt
|
||||||
|
template: CMakeLists.txt.tpl
|
||||||
|
- path: hello.c
|
||||||
|
template: hello.c.tpl
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{name} - empty base tree scaffolded by `pkh new`; there is intentionally no upstream build system here.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## The `empty` template: a metapackage (non-empty Depends list) or an
|
||||||
|
## empty base package with no build system at all — pure `dh $@` plumbing
|
||||||
|
## as a starting point for hand-written rules. Pure data: no hooks, the
|
||||||
|
## metapackage Depends payload travels in the wizard answers, and the
|
||||||
|
## only upstream file is the stub README marking the tree as
|
||||||
|
## intentionally empty.
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: empty
|
||||||
|
label: Metapackage / empty base (no build system)
|
||||||
|
detect:
|
||||||
|
files: []
|
||||||
|
build_depends: []
|
||||||
|
architecture: all
|
||||||
|
rules_dh_line: "dh $@"
|
||||||
|
files:
|
||||||
|
- path: README
|
||||||
|
template: README.tpl
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module {name}
|
||||||
|
|
||||||
|
go 1.21
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Placeholder for {name}, generated by `pkh new`.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Hello from {command}!")
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## The `go` template: a Go module built through dh-golang. The logic half
|
||||||
|
## — the go.mod module-line probe and the `{go_import_path}` value below —
|
||||||
|
## lives in src/new/templates/go.rs; the skeleton bodies are static data
|
||||||
|
## (the `go` directive of go.mod stays a literal: nothing about it is
|
||||||
|
## answer-derived, so it has no {placeholder}).
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: go
|
||||||
|
label: Go module
|
||||||
|
detect:
|
||||||
|
files: [go.mod]
|
||||||
|
build_depends:
|
||||||
|
- golang-any
|
||||||
|
- dh-golang
|
||||||
|
architecture: any
|
||||||
|
rules_dh_line: "dh $@ --buildsystem=golang"
|
||||||
|
source_fields:
|
||||||
|
XS-Go-Import-Path: "{go_import_path}"
|
||||||
|
files:
|
||||||
|
- path: go.mod
|
||||||
|
template: go.mod.tpl
|
||||||
|
- path: main.go
|
||||||
|
template: main.go.tpl
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -O2 -Wall -Wextra
|
||||||
|
PREFIX ?= /usr
|
||||||
|
|
||||||
|
all: {command}
|
||||||
|
|
||||||
|
{command}: hello.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ hello.c
|
||||||
|
|
||||||
|
install: {command}
|
||||||
|
install -Dm755 {command} $(DESTDIR)$(PREFIX)/bin/{command}
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f {command}
|
||||||
|
|
||||||
|
.PHONY: all install clean
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Placeholder for {name}, generated by `pkh new`. */
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("Hello from {command}!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{command} usr/bin/{command}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
## The `makefile` template: a generic project driven by a plain Makefile.
|
||||||
|
## debhelper's makefile buildsystem runs `make` for the build and
|
||||||
|
## `make install DESTDIR=...` when the Makefile carries an `install:`
|
||||||
|
## target (missing targets are skipped gracefully), so plain `dh $@`
|
||||||
|
## plumbing is enough here. The phony-install hint of
|
||||||
|
## src/new/templates/makefile.rs (whether dh_auto_install will run
|
||||||
|
## `make install` for an existing tree) is the only logic; the skeleton
|
||||||
|
## bodies below are static data (the install mapping is rendered for
|
||||||
|
## skeletons only, whose phony install target is known by construction).
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: makefile
|
||||||
|
label: Generic (Makefile)
|
||||||
|
detect:
|
||||||
|
files: [Makefile]
|
||||||
|
build_depends:
|
||||||
|
- build-essential
|
||||||
|
architecture: any
|
||||||
|
rules_dh_line: "dh $@"
|
||||||
|
files:
|
||||||
|
- path: hello.c
|
||||||
|
template: hello.c.tpl
|
||||||
|
- path: Makefile
|
||||||
|
template: Makefile.tpl
|
||||||
|
- path: debian/install
|
||||||
|
template: install.tpl
|
||||||
|
skeleton_only: true
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Placeholder for {name}, generated by `pkh new`. */
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("Hello from {command}!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
## The `meson` template: a C/C++ project built with Meson through the
|
||||||
|
## debhelper meson buildsystem. The skeleton bodies below are static data;
|
||||||
|
## the logic half — the project() probe and the wizard's pkg-config
|
||||||
|
## opt-in (appended to Build-Depends) — lives in src/new/templates/meson.rs.
|
||||||
|
##
|
||||||
|
## hello.c.tpl is deliberately duplicated (byte-identical) across the
|
||||||
|
## makefile, cmake and autotools template directories: every template
|
||||||
|
## directory is self-contained — the registry embeds each directory's
|
||||||
|
## bodies under its own entry — so a shared body would need
|
||||||
|
## cross-directory references the manifest schema has no machinery for.
|
||||||
|
## The duplication replaces the Rust hello_c() helper meson.rs used to
|
||||||
|
## lend cmake.rs and autotools.rs.
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: meson
|
||||||
|
label: C/C++ (Meson)
|
||||||
|
detect:
|
||||||
|
files: [meson.build]
|
||||||
|
build_depends:
|
||||||
|
- meson
|
||||||
|
architecture: any
|
||||||
|
rules_dh_line: "dh $@ --buildsystem=meson"
|
||||||
|
files:
|
||||||
|
- path: meson.build
|
||||||
|
template: meson.build.tpl
|
||||||
|
- path: hello.c
|
||||||
|
template: hello.c.tpl
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
project('{name}', version: '{upstream_version}', license: '{license}', default_options: ['c_std=c11'])
|
||||||
|
|
||||||
|
executable('{command}', 'hello.c', install: true)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Placeholder for {name}, generated by `pkh new`."""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Hello from {command}!")
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## The `python` template: a PEP 517 project built with pybuild. The
|
||||||
|
## skeleton bodies below are static data on the fresh-skeleton baseline
|
||||||
|
## (the setuptools backend): the module directory and the console-script
|
||||||
|
## entry point are named by the `{module_name}` placeholder python.rs
|
||||||
|
## derives from the package name — dpkg names may carry `+`/`.` and may
|
||||||
|
## start with a digit, none of which a Python module name may. The logic
|
||||||
|
## half — the pyproject.toml/setup.py probe and the Build-Depends /
|
||||||
|
## architecture resolution for existing projects (backend package,
|
||||||
|
## pyproject presence, C-extension hints) — lives in
|
||||||
|
## src/new/templates/python.rs.
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: python
|
||||||
|
label: Python (pyproject.toml / setup.py)
|
||||||
|
detect:
|
||||||
|
files: [pyproject.toml, setup.py, setup.cfg]
|
||||||
|
build_depends:
|
||||||
|
- dh-python
|
||||||
|
- python3-all
|
||||||
|
- pybuild-plugin-pyproject
|
||||||
|
- python3-setuptools
|
||||||
|
architecture: all
|
||||||
|
rules_dh_line: "dh $@ --with python3 --buildsystem=pybuild"
|
||||||
|
files:
|
||||||
|
- path: pyproject.toml
|
||||||
|
template: pyproject.toml.tpl
|
||||||
|
- path: "{module_name}/__init__.py"
|
||||||
|
template: __init__.py.tpl
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "{name}"
|
||||||
|
version = "{upstream_version}"
|
||||||
|
description = "{summary}"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
{command} = "{module_name}:main"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "{crate_name}"
|
||||||
|
version = "{upstream_version}"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Placeholder for {name}, generated by `pkh new`.
|
||||||
|
fn main() {
|
||||||
|
println!("Hello from {command}!");
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## The `rust` template: a vendored Cargo build (see the module docs of
|
||||||
|
## src/new/templates/rust.rs for the vendoring strategy). The skeleton
|
||||||
|
## bodies and the vendored-build rules overrides below are static data; the
|
||||||
|
## logic half — the cargo vendor post-write hook, the project probe, and
|
||||||
|
## the `{crate_name}` / `{locked}` / `{artifact}` values of the bodies —
|
||||||
|
## lives in that module (dpkg package names may carry `+`/`.`, which cargo
|
||||||
|
## rejects in crate names, so the skeleton crate name is a derived
|
||||||
|
## placeholder, not the raw `{name}`).
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: rust
|
||||||
|
label: Rust (Cargo.toml)
|
||||||
|
detect:
|
||||||
|
files: [Cargo.toml]
|
||||||
|
build_depends:
|
||||||
|
- cargo:native
|
||||||
|
- rustc:native
|
||||||
|
architecture: any
|
||||||
|
rules_dh_line: "dh $@"
|
||||||
|
# The vendored-build overrides appended to debian/rules; `--locked` is only
|
||||||
|
# used when the packaged tree already carries a Cargo.lock (the vendoring
|
||||||
|
# hook patches it in once it creates the lockfile), and the built artifact
|
||||||
|
# of a fresh skeleton is named after its crate.
|
||||||
|
rules_extra_file: rules.extra.tpl
|
||||||
|
gitignore_entries:
|
||||||
|
- vendor/
|
||||||
|
- .cargo/config.toml
|
||||||
|
files:
|
||||||
|
- path: Cargo.toml
|
||||||
|
template: Cargo.toml.tpl
|
||||||
|
- path: src/main.rs
|
||||||
|
template: main.rs.tpl
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
override_dh_auto_build:
|
||||||
|
cargo build --release --offline{locked}
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
install -Dm755 target/release/{artifact} debian/{name}/usr/bin/{command}
|
||||||
|
|
||||||
|
override_dh_auto_test:
|
||||||
|
cargo test --release --offline{locked}
|
||||||
|
|
||||||
|
override_dh_update_autotools_config:
|
||||||
|
|
||||||
|
override_dh_clean:
|
||||||
|
# dh_clean unlinks `*.orig` patch backups, but vendored crates
|
||||||
|
# ship files like `Cargo.toml.orig` that cargo's per-file
|
||||||
|
# checksums require on cold builds (chroots, Launchpad).
|
||||||
|
dh_clean -X .orig
|
||||||
|
|
||||||
|
override_dh_auto_clean:
|
||||||
|
cargo clean
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{command}.sh usr/bin/{command}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## The `shell` template: a single interpreted script installed to
|
||||||
|
## /usr/bin with plain `dh $@` plumbing. Detection is not marker-based: the
|
||||||
|
## single-script heuristic of src/new/detect.rs (a lone *.sh or shebang
|
||||||
|
## file) maps here. The probe pre-filling the wizard answers from the
|
||||||
|
## script file name lives in src/new/templates/shell.rs; everything else
|
||||||
|
## is the data below (the skeleton script is executable, the install
|
||||||
|
## mapping exists for skeletons only — packaging an existing tree leaves
|
||||||
|
## the mapping to the user).
|
||||||
|
##
|
||||||
|
## Schema: see src/new/templates/mod.rs.
|
||||||
|
|
||||||
|
id: shell
|
||||||
|
label: Shell script / single interpreted file
|
||||||
|
detect:
|
||||||
|
files: []
|
||||||
|
build_depends: []
|
||||||
|
architecture: all
|
||||||
|
rules_dh_line: "dh $@"
|
||||||
|
files:
|
||||||
|
- path: "{command}.sh"
|
||||||
|
template: script.tpl
|
||||||
|
executable: true
|
||||||
|
- path: debian/install
|
||||||
|
template: install.tpl
|
||||||
|
skeleton_only: true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Placeholder for {name}, generated by `pkh new`.
|
||||||
|
echo "Hello from {command}!"
|
||||||
+60
-24
@@ -4,6 +4,7 @@
|
|||||||
//! for mmdebstrap operations and for PPA packages by downloading them.
|
//! for mmdebstrap operations and for PPA packages by downloading them.
|
||||||
|
|
||||||
use crate::context;
|
use crate::context;
|
||||||
|
use crate::data::embed_data;
|
||||||
use crate::distro_info;
|
use crate::distro_info;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
@@ -11,6 +12,26 @@ use std::os::unix::fs::MetadataExt;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Keyserver endpoint, loaded from the bundled `keyserver.yml` data file
|
||||||
|
/// (same pattern as `distro_info.yml`): the lookup URL is a static
|
||||||
|
/// endpoint that was previously hardcoded in two modules.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct KeyserverData {
|
||||||
|
/// OpenPGP key lookup URL template (`{fingerprint}`)
|
||||||
|
lookup_template: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_data! {
|
||||||
|
static ref KEYSERVER_DATA: KeyserverData = "../../data/keyserver.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL fetching the OpenPGP key of `fingerprint` from the keyserver
|
||||||
|
pub(crate) fn keyserver_lookup_url(fingerprint: &str) -> String {
|
||||||
|
KEYSERVER_DATA
|
||||||
|
.lookup_template
|
||||||
|
.replace("{fingerprint}", fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
/// Launchpad API response structure for PPA information
|
/// Launchpad API response structure for PPA information
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct LaunchpadPpaResponse {
|
struct LaunchpadPpaResponse {
|
||||||
@@ -198,7 +219,12 @@ fn validate_keyring_dir(dir_uid: u32, mode: u32, euid: u32) -> Result<(), String
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and import a PPA key using Launchpad API
|
/// Download and import a PPA key using the Launchpad API
|
||||||
|
///
|
||||||
|
/// The signing key fingerprint is looked up through the shared HTTP client;
|
||||||
|
/// the key itself is fetched from the keyserver with curl through the
|
||||||
|
/// context, because the key file must land in the context's filesystem
|
||||||
|
/// (which may be remote).
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `ctx` - Optional context to use
|
/// * `ctx` - Optional context to use
|
||||||
@@ -229,33 +255,36 @@ pub async fn download_trust_ppa_key(
|
|||||||
ppa_name
|
ppa_name
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get PPA information from Launchpad API to get signing key fingerprint
|
// Get PPA information from the Launchpad API to get the signing key
|
||||||
// Use the correct devel API endpoint
|
// fingerprint. The query is context-independent metadata, so it goes
|
||||||
let api_url = format!(
|
// through the shared HTTP client (timeouts, retries) rather than
|
||||||
"https://api.launchpad.net/1.0/~{}/+archive/ubuntu/{}",
|
// shelling out to curl.
|
||||||
ppa_owner, ppa_name
|
let api_url = crate::launchpad::archive_url(ppa_owner, ppa_name);
|
||||||
);
|
|
||||||
log::debug!("Querying Launchpad API: {}", api_url);
|
log::debug!("Querying Launchpad API: {}", api_url);
|
||||||
|
|
||||||
let api_response = ctx
|
let response = distro_info::http_get_retried(&api_url).await.map_err(|e| {
|
||||||
.command("curl")
|
format!(
|
||||||
.arg("-s")
|
"Failed to query Launchpad API for PPA {}/{}: {}",
|
||||||
.arg("-f")
|
ppa_owner, ppa_name, e
|
||||||
.arg("-H")
|
)
|
||||||
.arg("Accept: application/json")
|
})?;
|
||||||
.arg(&api_url)
|
if !response.status().is_success() {
|
||||||
.output()?;
|
|
||||||
|
|
||||||
if !api_response.status.success() {
|
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Failed to query Launchpad API for PPA {}/{}",
|
"Failed to query Launchpad API for PPA {}/{}: HTTP {}",
|
||||||
ppa_owner, ppa_name
|
ppa_owner,
|
||||||
|
ppa_name,
|
||||||
|
response.status()
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the JSON response to extract the signing key fingerprint
|
// Parse the JSON response to extract the signing key fingerprint
|
||||||
let api_response_str = String::from_utf8_lossy(&api_response.stdout);
|
let api_response_str = response.text().await.map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"Failed to read the Launchpad API response for PPA {}/{}: {}",
|
||||||
|
ppa_owner, ppa_name, e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let ppa_response: LaunchpadPpaResponse =
|
let ppa_response: LaunchpadPpaResponse =
|
||||||
serde_json::from_str(&api_response_str).map_err(|e| {
|
serde_json::from_str(&api_response_str).map_err(|e| {
|
||||||
format!(
|
format!(
|
||||||
@@ -268,10 +297,7 @@ pub async fn download_trust_ppa_key(
|
|||||||
log::debug!("Found PPA signing key fingerprint: {}", fingerprint);
|
log::debug!("Found PPA signing key fingerprint: {}", fingerprint);
|
||||||
|
|
||||||
// Download the actual key from the keyserver using the fingerprint
|
// Download the actual key from the keyserver using the fingerprint
|
||||||
let keyserver_url = format!(
|
let keyserver_url = keyserver_lookup_url(&fingerprint);
|
||||||
"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x{}",
|
|
||||||
fingerprint
|
|
||||||
);
|
|
||||||
log::debug!("Downloading key from keyserver: {}", keyserver_url);
|
log::debug!("Downloading key from keyserver: {}", keyserver_url);
|
||||||
|
|
||||||
let mut curl_cmd = ctx.command("curl");
|
let mut curl_cmd = ctx.command("curl");
|
||||||
@@ -306,6 +332,16 @@ pub async fn download_trust_ppa_key(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// The data-driven template renders the lookup URL the hardcoded
|
||||||
|
/// format! used to build (verified against the live keyserver)
|
||||||
|
#[test]
|
||||||
|
fn keyserver_lookup_url_substitutes_the_fingerprint() {
|
||||||
|
assert_eq!(
|
||||||
|
keyserver_lookup_url("0123456789ABCDEF"),
|
||||||
|
"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x0123456789ABCDEF"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_keyring_dir_accepts_private_dir_owned_by_current_user() {
|
fn test_validate_keyring_dir_accepts_private_dir_owned_by_current_user() {
|
||||||
assert!(validate_keyring_dir(1000, 0o700, 1000).is_ok());
|
assert!(validate_keyring_dir(1000, 0o700, 1000).is_ok());
|
||||||
|
|||||||
+3
-3
@@ -851,7 +851,7 @@ fn parse_ppa_url(ppa_base_url: &str) -> Option<(String, String)> {
|
|||||||
.strip_prefix("https://")
|
.strip_prefix("https://")
|
||||||
.or_else(|| ppa_base_url.strip_prefix("http://"))?;
|
.or_else(|| ppa_base_url.strip_prefix("http://"))?;
|
||||||
let (host, path) = rest.split_once('/')?;
|
let (host, path) = rest.split_once('/')?;
|
||||||
if host != "ppa.launchpadcontent.net" {
|
if host != crate::launchpad::ppa_content_host() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,7 +886,7 @@ pub async fn ppa_keyring_bytes(
|
|||||||
return Ok(cached.clone());
|
return Ok(cached.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let api_url = format!("https://api.launchpad.net/1.0/~{owner}/+archive/ubuntu/{name}");
|
let api_url = crate::launchpad::archive_url(&owner, &name);
|
||||||
let response = crate::distro_info::http_get_retried(&api_url).await?;
|
let response = crate::distro_info::http_get_retried(&api_url).await?;
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -905,7 +905,7 @@ pub async fn ppa_keyring_bytes(
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let key_url = format!("https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x{fingerprint}");
|
let key_url = crate::apt::keyring::keyserver_lookup_url(&fingerprint);
|
||||||
let armored = fetch_keyring_cached(&key_url).await?;
|
let armored = fetch_keyring_cached(&key_url).await?;
|
||||||
let keyring = dearmor(&armored)
|
let keyring = dearmor(&armored)
|
||||||
.map_err(|e| format!("invalid PGP armor in the key of PPA '{owner}/{name}': {e}"))?;
|
.map_err(|e| format!("invalid PGP armor in the key of PPA '{owner}/{name}': {e}"))?;
|
||||||
|
|||||||
+16
-8
@@ -135,15 +135,15 @@ pub fn vendor_from_origins_content(content: &str) -> Option<String> {
|
|||||||
|
|
||||||
/// Default build profiles applied by vendor hooks.
|
/// Default build profiles applied by vendor hooks.
|
||||||
///
|
///
|
||||||
/// The Ubuntu vendor module activates `derivative.ubuntu noudeb` by default;
|
/// The distro data carries them (`build_profiles` of the vendor's
|
||||||
/// Debian applies none. This mirrors what `Dpkg::BuildProfiles` resolves when
|
/// distribution in `data/distro_info.yml` — the Ubuntu vendor activates
|
||||||
/// `DEB_BUILD_PROFILES` is unset.
|
/// `derivative.ubuntu noudeb`, Debian applies none), mirroring what
|
||||||
|
/// `Dpkg::BuildProfiles` resolves when `DEB_BUILD_PROFILES` is unset. The
|
||||||
|
/// vendor is matched case-insensitively against the distro data keys
|
||||||
|
/// (dpkg's `Vendor:` field keeps its original casing); a vendor with no
|
||||||
|
/// distro entry gets no profiles.
|
||||||
pub fn default_build_profiles(vendor: &str) -> Vec<String> {
|
pub fn default_build_profiles(vendor: &str) -> Vec<String> {
|
||||||
if vendor.eq_ignore_ascii_case("ubuntu") {
|
crate::distro_info::get_build_profiles(&vendor.to_lowercase()).unwrap_or_default()
|
||||||
vec!["derivative.ubuntu".to_string(), "noudeb".to_string()]
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the active build profiles: explicit `-P` profiles take precedence,
|
/// Resolve the active build profiles: explicit `-P` profiles take precedence,
|
||||||
@@ -378,6 +378,14 @@ mod tests {
|
|||||||
default_build_profiles("ubuntu"),
|
default_build_profiles("ubuntu"),
|
||||||
vec!["derivative.ubuntu".to_string(), "noudeb".to_string()]
|
vec!["derivative.ubuntu".to_string(), "noudeb".to_string()]
|
||||||
);
|
);
|
||||||
|
// dpkg's Vendor field keeps its original casing; the distro data
|
||||||
|
// keys are lowercase.
|
||||||
|
assert_eq!(
|
||||||
|
default_build_profiles("Ubuntu"),
|
||||||
|
vec!["derivative.ubuntu".to_string(), "noudeb".to_string()]
|
||||||
|
);
|
||||||
|
// A vendor without a distro entry gets no profiles.
|
||||||
|
assert!(default_build_profiles("some-derivative").is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
//! Embedding convention for the static reference data files (`data/*.yml`)
|
||||||
|
//!
|
||||||
|
//! Reference data that changes independently of the code — distro series
|
||||||
|
//! pointers, pinned SSH host keys, package quirks — lives in YAML files
|
||||||
|
//! under `data/` at the repo root instead of hardcoded in the source, so
|
||||||
|
//! it is updatable in one reviewable place.
|
||||||
|
//!
|
||||||
|
//! This module is deliberately not a central registry: each file is
|
||||||
|
//! embedded by the module that owns it (distro_info.rs owns
|
||||||
|
//! data/distro_info.yml, launchpad.rs owns data/launchpad.yml,
|
||||||
|
//! apt/keyring.rs owns data/keyserver.yml, new/origin.rs owns
|
||||||
|
//! data/forges.yml, put/ssh.rs owns data/host_keys.yml, quirks.rs owns
|
||||||
|
//! data/quirks.yml) through the [`embed_data!`] macro below, so data
|
||||||
|
//! and its accessors stay together and a diff touching one domain cannot
|
||||||
|
//! half-touch another. The macro embeds the file at compile time and
|
||||||
|
//! parses it once into a `lazy_static` on first use; since the data ships
|
||||||
|
//! inside the binary, a parse failure is a build-time bug that cannot be
|
||||||
|
//! recovered from at runtime, and the macro panics on it.
|
||||||
|
//!
|
||||||
|
//! Paths and URLs in the data files carry their variable parts as `{name}`
|
||||||
|
//! placeholders, substituted with `str::replace` at the use site — no
|
||||||
|
//! template engine.
|
||||||
|
|
||||||
|
/// Embed one YAML data file as a lazily-parsed static, following the
|
||||||
|
/// convention documented at the module level.
|
||||||
|
///
|
||||||
|
/// Takes the visibility of the generated static (none for private, `pub` or
|
||||||
|
/// `pub(crate)`-style), its name, its struct type (which stays defined in
|
||||||
|
/// the owning module, next to its accessors) and the file path relative to
|
||||||
|
/// the invoking source file (`"../data/distro_info.yml"` from
|
||||||
|
/// `src/distro_info.rs`, `"../../data/host_keys.yml"` from
|
||||||
|
/// `src/put/ssh.rs`, ...), and expands to the house `include_str!` →
|
||||||
|
/// `lazy_static` → parse pattern — only the embed+parse boilerplate is
|
||||||
|
/// generated.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// embed_data! {
|
||||||
|
/// static ref MY_DATA: MyData = "../data/my_data.yml"
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
macro_rules! embed_data {
|
||||||
|
// Internal arm: the visibility arrives wrapped in parentheses (empty for
|
||||||
|
// private statics) because `lazy_static!` only re-matches literal
|
||||||
|
// `pub`/`pub(...)` token sequences, not an opaque forwarded `vis`.
|
||||||
|
(@expand ($($vis:tt)*) static ref $name:ident : $ty:ty = $path:literal) => {
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
// The YAML is include_str!'d at compile time and statically
|
||||||
|
// valid; if it ever failed to parse it would be a build-time bug
|
||||||
|
// that cannot be recovered from at runtime, so panicking here is
|
||||||
|
// acceptable.
|
||||||
|
$($vis)* static ref $name: $ty = serde_yaml::from_str(include_str!($path))
|
||||||
|
.expect(concat!(
|
||||||
|
"built-in ",
|
||||||
|
$path,
|
||||||
|
" data is statically valid and must parse"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(static ref $name:ident : $ty:ty = $path:literal) => {
|
||||||
|
$crate::data::embed_data!(@expand () static ref $name : $ty = $path);
|
||||||
|
};
|
||||||
|
(pub static ref $name:ident : $ty:ty = $path:literal) => {
|
||||||
|
$crate::data::embed_data!(@expand (pub) static ref $name : $ty = $path);
|
||||||
|
};
|
||||||
|
(pub ($($vis:tt)+) static ref $name:ident : $ty:ty = $path:literal) => {
|
||||||
|
$crate::data::embed_data!(@expand (pub ($($vis)+)) static ref $name : $ty = $path);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes the macro available through the module path
|
||||||
|
/// (`use crate::data::embed_data;`)
|
||||||
|
pub(crate) use embed_data;
|
||||||
+108
-53
@@ -67,8 +67,41 @@ pub fn setup_environment(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The suites a cross-build environment enables for `series`: the series
|
||||||
|
/// itself plus the distro data's cross pockets (`<series>-updates`,
|
||||||
|
/// `<series>-backports`, `<series>-security`), plus the explicitly
|
||||||
|
/// requested `pocket` when one is given ('proposed' stays opt-in exactly
|
||||||
|
/// this way — it is not a cross pocket). Shared by the source-adjusting
|
||||||
|
/// pass and the added mirror entry, which used to duplicate the list.
|
||||||
|
fn cross_suites(
|
||||||
|
series: &str,
|
||||||
|
pocket: Option<&str>,
|
||||||
|
dist: &str,
|
||||||
|
) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
let mut suites = vec![series.to_string()];
|
||||||
|
for p in crate::distro_info::get_cross_pockets(dist)? {
|
||||||
|
suites.push(format!("{series}-{p}"));
|
||||||
|
}
|
||||||
|
if let Some(p) = pocket {
|
||||||
|
let pocket_suite = format!("{series}-{p}");
|
||||||
|
if !suites.contains(&pocket_suite) {
|
||||||
|
suites.push(pocket_suite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(suites)
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure that repositories for target architecture are available
|
/// Ensure that repositories for target architecture are available
|
||||||
/// This also handles the 'ports.ubuntu.com' vs 'archive.ubuntu.com' on Ubuntu
|
///
|
||||||
|
/// On Ubuntu hosts, driven by the bundled distro data
|
||||||
|
/// (`data/distro_info.yml`): the official sources served by the mirror of
|
||||||
|
/// the local architecture (the primary archive and its security sibling)
|
||||||
|
/// are scoped to it and carry every component and cross-build suite, and
|
||||||
|
/// the mirror serving the target architecture (ports, for the non-local
|
||||||
|
/// ones) is added when no existing source serves the arch from it yet.
|
||||||
|
/// Debian hosts are left alone: one mirror serves every architecture, so
|
||||||
|
/// the host's own sources already cover the target — the os-release gate
|
||||||
|
/// below is what makes that a data conclusion instead of hardcoding.
|
||||||
pub fn ensure_repositories(
|
pub fn ensure_repositories(
|
||||||
arch: &str,
|
arch: &str,
|
||||||
series: &str,
|
series: &str,
|
||||||
@@ -97,90 +130,78 @@ pub fn ensure_repositories(
|
|||||||
if !os_release.contains("ID=ubuntu") {
|
if !os_release.contains("ID=ubuntu") {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let dist = "ubuntu";
|
||||||
|
|
||||||
// Load existing sources
|
// Load existing sources
|
||||||
let mut sources = crate::apt::sources::load(Some(ctx.clone()))?;
|
let mut sources = crate::apt::sources::load(Some(ctx.clone()))?;
|
||||||
|
|
||||||
|
// The mirrors serving each side of the cross build (primary for the
|
||||||
|
// local architectures, ports for the others) and the distro data's
|
||||||
|
// components and suites
|
||||||
|
let local_mirror = crate::distro_info::mirror_for_arch(dist, &local_arch)?;
|
||||||
|
let target_mirror = crate::distro_info::mirror_for_arch(dist, arch)?;
|
||||||
|
let components = crate::distro_info::get_dist_components(dist)?;
|
||||||
|
let required_suites = cross_suites(series, pocket, dist)?;
|
||||||
|
|
||||||
// Ensure all components are enabled for the primary architecture
|
// Ensure all components are enabled for the primary architecture
|
||||||
for source in &mut sources {
|
for source in &mut sources {
|
||||||
if source.uri.contains("archive.ubuntu.com") || source.uri.contains("security.ubuntu.com") {
|
// Official sources served by the local mirror (the primary archive
|
||||||
|
// and its security sibling); ports serves the other architectures
|
||||||
|
// and is configured below instead
|
||||||
|
if !crate::distro_info::is_mirror_source(local_mirror, &source.uri) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Scope to local_arch if not already scoped
|
// Scope to local_arch if not already scoped
|
||||||
if source.architectures.is_empty() {
|
if source.architectures.is_empty() {
|
||||||
source.architectures.push(local_arch.clone());
|
source.architectures.push(local_arch.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all components are present
|
// Ensure all components are present
|
||||||
let required_components = ["main", "restricted", "universe", "multiverse"];
|
for comp in &components {
|
||||||
for &comp in &required_components {
|
if !source.components.contains(comp) {
|
||||||
if !source.components.contains(&comp.to_string()) {
|
source.components.push(comp.clone());
|
||||||
source.components.push(comp.to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all suites (pockets) are enabled, excluding 'proposed'
|
// Ensure all suites (pockets) are enabled
|
||||||
// unless explicitly requested through the 'pocket' option
|
for suite in &required_suites {
|
||||||
let mut required_suites = vec![
|
if !source.suite.contains(suite) {
|
||||||
series.to_string(),
|
source.suite.push(suite.clone());
|
||||||
format!("{}-updates", series),
|
|
||||||
format!("{}-backports", series),
|
|
||||||
format!("{}-security", series),
|
|
||||||
];
|
|
||||||
if let Some(p) = pocket {
|
|
||||||
let pocket_suite = format!("{series}-{p}");
|
|
||||||
if !required_suites.contains(&pocket_suite) {
|
|
||||||
required_suites.push(pocket_suite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for suite in required_suites {
|
|
||||||
if !source.suite.contains(&suite) {
|
|
||||||
source.suite.push(suite);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if ports repository already exists for the target architecture
|
// Check whether an existing source already serves the target
|
||||||
let has_ports = sources
|
// architecture from its mirror (e.g. the ports mirror for a
|
||||||
.iter()
|
// non-local arch); when cross-building for the local architecture,
|
||||||
.any(|s| s.uri.contains("ports.ubuntu.com") && s.architectures.contains(&arch.to_string()));
|
// the primary sources above already do
|
||||||
|
let has_target = sources.iter().any(|s| {
|
||||||
|
crate::distro_info::is_mirror_source(target_mirror, &s.uri)
|
||||||
|
&& s.architectures.contains(&arch.to_string())
|
||||||
|
});
|
||||||
|
|
||||||
if !has_ports {
|
if !has_target {
|
||||||
// Add ports repository for the target architecture
|
// Add the target architecture's mirror (ports for the non-local
|
||||||
let mut ports_suites = vec![
|
// architectures on Ubuntu)
|
||||||
series.to_string(),
|
let mirror_entry = crate::apt::sources::SourceEntry {
|
||||||
format!("{series}-updates"),
|
|
||||||
format!("{series}-backports"),
|
|
||||||
format!("{series}-security"),
|
|
||||||
];
|
|
||||||
if let Some(p) = pocket {
|
|
||||||
let pocket_suite = format!("{series}-{p}");
|
|
||||||
if !ports_suites.contains(&pocket_suite) {
|
|
||||||
ports_suites.push(pocket_suite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let ports_entry = crate::apt::sources::SourceEntry {
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
kind: crate::apt::sources::SourceKind::Deb,
|
kind: crate::apt::sources::SourceKind::Deb,
|
||||||
components: vec![
|
components: components.clone(),
|
||||||
"main".to_string(),
|
|
||||||
"restricted".to_string(),
|
|
||||||
"universe".to_string(),
|
|
||||||
"multiverse".to_string(),
|
|
||||||
],
|
|
||||||
architectures: vec![arch.to_string()],
|
architectures: vec![arch.to_string()],
|
||||||
uri: "http://ports.ubuntu.com/ubuntu-ports".to_string(),
|
uri: target_mirror.url.clone(),
|
||||||
signed_by: None,
|
signed_by: None,
|
||||||
trusted: None,
|
trusted: None,
|
||||||
suite: ports_suites,
|
suite: required_suites.clone(),
|
||||||
// No origin: saved to the pkh-owned added-sources file
|
// No origin: saved to the pkh-owned added-sources file
|
||||||
origin: None,
|
origin: None,
|
||||||
};
|
};
|
||||||
sources.push(ports_entry);
|
sources.push(mirror_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the updated sources: each entry is written back to its origin
|
// Save the updated sources: each entry is written back to its origin
|
||||||
// file in its own format (keeping its own Signed-By and Enabled state),
|
// file in its own format (keeping its own Signed-By and Enabled state),
|
||||||
// and the new ports entry goes to the pkh-owned added-sources file
|
// and the new mirror entry goes to the pkh-owned added-sources file
|
||||||
crate::apt::sources::save(Some(ctx.clone()), sources)?;
|
crate::apt::sources::save(Some(ctx.clone()), sources)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -234,4 +255,38 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(env.len(), 2);
|
assert_eq!(env.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The suite list a cross-build environment enables: the series, its
|
||||||
|
/// cross pockets from the distro data (updates, backports, security),
|
||||||
|
/// and the explicitly requested pocket — which is the only way
|
||||||
|
/// 'proposed' gets in.
|
||||||
|
#[test]
|
||||||
|
fn test_cross_suites_from_distro_data() {
|
||||||
|
assert_eq!(
|
||||||
|
cross_suites("noble", None, "ubuntu").unwrap(),
|
||||||
|
vec![
|
||||||
|
"noble".to_string(),
|
||||||
|
"noble-updates".to_string(),
|
||||||
|
"noble-backports".to_string(),
|
||||||
|
"noble-security".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// An explicitly requested pocket is added (not duplicated when it
|
||||||
|
// is already a cross pocket).
|
||||||
|
assert_eq!(
|
||||||
|
cross_suites("noble", Some("proposed"), "ubuntu").unwrap(),
|
||||||
|
vec![
|
||||||
|
"noble".to_string(),
|
||||||
|
"noble-updates".to_string(),
|
||||||
|
"noble-backports".to_string(),
|
||||||
|
"noble-security".to_string(),
|
||||||
|
"noble-proposed".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cross_suites("noble", Some("updates"), "ubuntu").unwrap(),
|
||||||
|
cross_suites("noble", None, "ubuntu").unwrap()
|
||||||
|
);
|
||||||
|
assert!(cross_suites("noble", None, "not-a-distro").is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -135,13 +135,23 @@ pub async fn build(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UBUNTU: Ensure 'universe' repository is enabled
|
// UBUNTU: Ensure the 'universe' component is enabled on official
|
||||||
|
// Ubuntu sources (many build dependencies live there). The old
|
||||||
|
// `uri.contains("ubuntu")` gate also caught third-party repositories
|
||||||
|
// whose URL merely mentions Ubuntu; the mirror-data check leaves them
|
||||||
|
// alone. 'universe' is only added when Ubuntu's component list still
|
||||||
|
// carries it.
|
||||||
|
let ubuntu_components = crate::distro_info::get_dist_components("ubuntu")?;
|
||||||
|
if ubuntu_components.iter().any(|c| c == "universe") {
|
||||||
for source in &mut sources {
|
for source in &mut sources {
|
||||||
if source.uri.contains("ubuntu") && !source.components.contains(&"universe".to_string()) {
|
if crate::distro_info::is_official_source("ubuntu", &source.uri)
|
||||||
|
&& !source.components.contains(&"universe".to_string())
|
||||||
|
{
|
||||||
source.components.push("universe".to_string());
|
source.components.push("universe".to_string());
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Enable the requested pocket on archive sources, so build-dependencies
|
// Enable the requested pocket on archive sources, so build-dependencies
|
||||||
// are resolved from that pocket
|
// are resolved from that pocket
|
||||||
|
|||||||
+33
-5
@@ -366,12 +366,15 @@ fn find_dsc_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether an apt source URI points to a distribution archive
|
/// Check whether an apt source URI points to a distribution archive
|
||||||
/// (as opposed to a PPA or another third-party repository)
|
/// (as opposed to a PPA or another third-party repository): a thin,
|
||||||
|
/// dist-agnostic wrapper over [`crate::distro_info::is_official_source`],
|
||||||
|
/// unioned across every known distro. Where the distribution is known,
|
||||||
|
/// the dist-scoped check is preferred (it cannot misfire on another
|
||||||
|
/// distro's mirror); this fallback stays for the sites that cannot know.
|
||||||
pub(crate) fn is_archive_source(uri: &str) -> bool {
|
pub(crate) fn is_archive_source(uri: &str) -> bool {
|
||||||
uri.contains("archive.ubuntu.com")
|
crate::distro_info::supported_dists()
|
||||||
|| uri.contains("security.ubuntu.com")
|
.iter()
|
||||||
|| uri.contains("ports.ubuntu.com")
|
.any(|dist| crate::distro_info::is_official_source(dist, uri))
|
||||||
|| uri.contains("deb.debian.org")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -379,6 +382,31 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// The archive check is dist-agnostic (any distro's official mirror
|
||||||
|
/// counts) and host-based: the country mirrors the old substring
|
||||||
|
/// checks matched (`fr.archive.ubuntu.com`) still count, look-alike
|
||||||
|
/// hosts and PPAs do not.
|
||||||
|
#[test]
|
||||||
|
fn archive_sources_are_official_mirrors_of_any_distro() {
|
||||||
|
for uri in [
|
||||||
|
"https://archive.ubuntu.com/ubuntu",
|
||||||
|
// Country mirrors front the Ubuntu archive.
|
||||||
|
"http://fr.archive.ubuntu.com/ubuntu",
|
||||||
|
"http://security.ubuntu.com/ubuntu",
|
||||||
|
"http://ports.ubuntu.com/ubuntu-ports",
|
||||||
|
"https://deb.debian.org/debian",
|
||||||
|
] {
|
||||||
|
assert!(is_archive_source(uri), "{uri}");
|
||||||
|
}
|
||||||
|
for uri in [
|
||||||
|
"https://ppa.launchpadcontent.net/user/ppa/ubuntu",
|
||||||
|
"http://notarchive.ubuntu.com/ubuntu",
|
||||||
|
"https://example.com/debian",
|
||||||
|
] {
|
||||||
|
assert!(!is_archive_source(uri), "{uri}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn test_build_end_to_end(
|
async fn test_build_end_to_end(
|
||||||
package: &str,
|
package: &str,
|
||||||
series: &str,
|
series: &str,
|
||||||
|
|||||||
+336
-34
@@ -1,6 +1,8 @@
|
|||||||
|
use crate::data::embed_data;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -28,29 +30,76 @@ struct SeriesInfo {
|
|||||||
network: String,
|
network: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Architectures an archive mirror serves: an explicit list, or the `all`
|
||||||
|
/// sentinel meaning one mirror serves every architecture (Debian's mirror
|
||||||
|
/// setup — an exhaustive list would rot each time an arch is added)
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum MirrorArchs {
|
||||||
|
/// The `all` sentinel
|
||||||
|
All(String),
|
||||||
|
/// An explicit list of dpkg architecture names
|
||||||
|
List(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MirrorArchs {
|
||||||
|
/// Whether the mirror serves `arch`. A scalar other than the `all`
|
||||||
|
/// sentinel serves nothing (a test locks that reading of the data).
|
||||||
|
fn serves(&self, arch: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
MirrorArchs::All(sentinel) => sentinel == "all",
|
||||||
|
MirrorArchs::List(archs) => archs.iter().any(|a| a == arch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One archive mirror of a distribution: a URL serving a set of
|
||||||
|
/// architectures, plus the sibling host serving its `-security` pocket
|
||||||
|
/// for the same architectures (ports mirrors serve their own security)
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Mirror {
|
||||||
|
/// Base URL of the mirror (the primary mirror's URL doubles as the
|
||||||
|
/// dist's base URL, see [`get_base_url`])
|
||||||
|
pub url: String,
|
||||||
|
/// Sibling host serving the `-security` pocket for these
|
||||||
|
/// architectures; `None` when the mirror serves its own security
|
||||||
|
#[serde(default)]
|
||||||
|
security_url: Option<String>,
|
||||||
|
/// Architectures the mirror serves (see [`MirrorArchs`])
|
||||||
|
archs: MirrorArchs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mirror {
|
||||||
|
/// Whether the mirror serves `arch`
|
||||||
|
pub fn serves(&self, arch: &str) -> bool {
|
||||||
|
self.archs.serves(arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct DistData {
|
struct DistData {
|
||||||
base_url: String,
|
mirrors: HashMap<String, Mirror>,
|
||||||
archive_keyring: String,
|
archive_keyring: String,
|
||||||
pockets: Vec<String>,
|
pockets: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
sections: Vec<String>,
|
sections: Vec<String>,
|
||||||
|
components: Vec<String>,
|
||||||
|
cross_pockets: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
build_profiles: Vec<String>,
|
||||||
series: SeriesInfo,
|
series: SeriesInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Data {
|
struct Data {
|
||||||
dist: std::collections::HashMap<String, DistData>,
|
dist: HashMap<String, DistData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DATA_YAML: &str = include_str!("../distro_info.yml");
|
embed_data! {
|
||||||
lazy_static! {
|
static ref DATA: Data = "../data/distro_info.yml"
|
||||||
// The YAML is include_str!'d at compile time and statically valid; if it
|
}
|
||||||
// ever failed to parse it would be a build-time bug that cannot be
|
|
||||||
// recovered from at runtime, so panicking here is acceptable.
|
|
||||||
static ref DATA: Data = serde_yaml::from_str(DATA_YAML)
|
|
||||||
.expect("built-in distro_info.yml data is statically valid and must parse");
|
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
// Shared HTTP client used for all outgoing plain requests: timeouts keep
|
// Shared HTTP client used for all outgoing plain requests: timeouts keep
|
||||||
// a hanging remote (connect or transfer) from stalling pkh indefinitely.
|
// a hanging remote (connect or transfer) from stalling pkh indefinitely.
|
||||||
// The short pool idle timeout and TCP keepalive avoid reusing keep-alive
|
// The short pool idle timeout and TCP keepalive avoid reusing keep-alive
|
||||||
@@ -214,9 +263,30 @@ fn parse_series_csv(content: &str) -> Result<Vec<SeriesInformation>, Box<dyn Err
|
|||||||
Ok(series_info_list)
|
Ok(series_info_list)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List the distributions known to pkh (e.g. "debian", "ubuntu")
|
/// List the distributions known to pkh (e.g. "debian", "ubuntu"), sorted so
|
||||||
|
/// that menus and error messages derived from it are deterministic
|
||||||
pub fn supported_dists() -> Vec<String> {
|
pub fn supported_dists() -> Vec<String> {
|
||||||
DATA.dist.keys().cloned().collect()
|
let mut dists: Vec<String> = DATA.dist.keys().cloned().collect();
|
||||||
|
dists.sort();
|
||||||
|
dists
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of a dist's primary mirror entry: its URL doubles as the dist's
|
||||||
|
/// base URL ([`get_base_url`]) and is the first candidate of
|
||||||
|
/// [`mirror_for_arch`]
|
||||||
|
const PRIMARY_MIRROR: &str = "primary";
|
||||||
|
|
||||||
|
/// The data of a known distribution: the shared "unknown distribution"
|
||||||
|
/// error of the per-dist accessors
|
||||||
|
fn dist_data(dist: &str) -> Result<&'static DistData, Box<dyn Error>> {
|
||||||
|
DATA.dist.get(dist).ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Unknown distribution '{}'. Supported distributions are: {}.",
|
||||||
|
dist,
|
||||||
|
supported_dists().join(", ")
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Special changelog distribution marking an entry that has not been
|
/// Special changelog distribution marking an entry that has not been
|
||||||
@@ -240,7 +310,7 @@ pub async fn get_ordered_series(dist: &str) -> Result<Vec<SeriesInformation>, Bo
|
|||||||
})?;
|
})?;
|
||||||
let series_info = &dist_data.series;
|
let series_info = &dist_data.series;
|
||||||
let content = if Path::new(series_info.local.as_str()).exists() {
|
let content = if Path::new(series_info.local.as_str()).exists() {
|
||||||
std::fs::read_to_string(format!("/usr/share/distro-info/{dist}.csv")).map_err(|e| {
|
std::fs::read_to_string(series_info.local.as_str()).map_err(|e| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to read distribution series data for '{dist}' \
|
"Failed to read distribution series data for '{dist}' \
|
||||||
from '{}': {}. The 'distro-info' package provides these CSV files.",
|
from '{}': {}. The 'distro-info' package provides these CSV files.",
|
||||||
@@ -351,14 +421,7 @@ pub async fn get_dist_from_series(series: &str) -> Result<String, Box<dyn Error>
|
|||||||
///
|
///
|
||||||
/// Example: get_dist_pockets(ubuntu) => ["", "updates", "security", "proposed"]
|
/// Example: get_dist_pockets(ubuntu) => ["", "updates", "security", "proposed"]
|
||||||
pub fn get_dist_pockets(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
pub fn get_dist_pockets(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
let dist_data = DATA.dist.get(dist).ok_or_else(|| {
|
let mut pockets = dist_data(dist)?.pockets.clone();
|
||||||
format!(
|
|
||||||
"Unknown distribution '{}'. Supported distributions are: {}.",
|
|
||||||
dist,
|
|
||||||
supported_dists().join(", ")
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut pockets = dist_data.pockets.clone();
|
|
||||||
|
|
||||||
// Explicitely add 'main' pocket, which is just the empty string, first
|
// Explicitely add 'main' pocket, which is just the empty string, first
|
||||||
pockets.insert(0, "".to_string());
|
pockets.insert(0, "".to_string());
|
||||||
@@ -366,18 +429,38 @@ pub fn get_dist_pockets(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
|||||||
Ok(pockets)
|
Ok(pockets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the archive components of a distribution (ubuntu's main,
|
||||||
|
/// restricted, universe, multiverse; Debian's main, contrib, non-free,
|
||||||
|
/// non-free-firmware): the default set a build environment enables on its
|
||||||
|
/// official sources. Live archive operations keep resolving components
|
||||||
|
/// from Release files ([`get_components`]); this is the offline default.
|
||||||
|
pub fn get_dist_components(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
Ok(dist_data(dist)?.components.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the pockets a cross-build environment enables for a series (the
|
||||||
|
/// `<series>-<pocket>` suite list is built from these): updates,
|
||||||
|
/// backports and security. Deliberately separate from
|
||||||
|
/// [`get_dist_pockets`], which is the *search order* of pull — folding
|
||||||
|
/// backports into it would change pull behavior.
|
||||||
|
pub fn get_cross_pockets(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
Ok(dist_data(dist)?.cross_pockets.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default build profiles of a distribution's vendor (Ubuntu
|
||||||
|
/// activates `derivative.ubuntu noudeb`, Debian none), mirroring what
|
||||||
|
/// `Dpkg::BuildProfiles` resolves when `DEB_BUILD_PROFILES` is unset.
|
||||||
|
/// Vendors are matched case-insensitively by the caller (dpkg's `Vendor:`
|
||||||
|
/// field keeps its original casing).
|
||||||
|
pub fn get_build_profiles(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
Ok(dist_data(dist)?.build_profiles.clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the valid `Section` values of a distribution's packages, as accepted
|
/// Get the valid `Section` values of a distribution's packages, as accepted
|
||||||
/// by its archives (a `section/subsection` in debian/control validates on
|
/// by its archives (a `section/subsection` in debian/control validates on
|
||||||
/// the part before the '/')
|
/// the part before the '/')
|
||||||
pub fn get_sections(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
pub fn get_sections(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
let dist_data = DATA.dist.get(dist).ok_or_else(|| {
|
Ok(dist_data(dist)?.sections.clone())
|
||||||
format!(
|
|
||||||
"Unknown distribution '{}'. Supported distributions are: {}.",
|
|
||||||
dist,
|
|
||||||
supported_dists().join(", ")
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok(dist_data.sections.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the sources URL for a distribution, series, pocket, and component
|
/// Get the sources URL for a distribution, series, pocket, and component
|
||||||
@@ -390,23 +473,105 @@ pub fn get_sources_url(base_url: &str, series: &str, pocket: &str, component: &s
|
|||||||
format!("{base_url}/dists/{series}{pocket_full}/{component}/source/Sources.gz")
|
format!("{base_url}/dists/{series}{pocket_full}/{component}/source/Sources.gz")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the archive base URL for a distribution
|
/// Get the archive base URL for a distribution: the URL of its primary
|
||||||
|
/// mirror (the former `base_url` key folded into `mirrors.primary.url`
|
||||||
|
/// when the mirrors were modeled — the signature is kept so the pull
|
||||||
|
/// paths do not churn)
|
||||||
///
|
///
|
||||||
/// Example: ubuntu => https://archive.ubuntu.com/ubuntu
|
/// Example: ubuntu => https://archive.ubuntu.com/ubuntu
|
||||||
pub fn get_base_url(dist: &str) -> Result<String, Box<dyn Error>> {
|
pub fn get_base_url(dist: &str) -> Result<String, Box<dyn Error>> {
|
||||||
DATA.dist
|
let mirror = dist_data(dist)?
|
||||||
.get(dist)
|
.mirrors
|
||||||
.map(|d| d.base_url.clone())
|
.get(PRIMARY_MIRROR)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
"Unknown distribution '{}'. Supported distributions are: {}.",
|
"Distribution '{dist}' has no '{PRIMARY_MIRROR}' mirror in the built-in \
|
||||||
dist,
|
configuration. This is a bug; supported distributions are: {}.",
|
||||||
|
supported_dists().join(", ")
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(mirror.url.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mirror of `dist` serving `arch`: the primary mirror first, then
|
||||||
|
/// the other mirrors by name, so the answer is deterministic. Debian's
|
||||||
|
/// `all` sentinel makes its primary mirror serve every architecture.
|
||||||
|
/// Errors when the dist is unknown or no mirror serves the architecture
|
||||||
|
/// (an architecture the built-in data does not know about).
|
||||||
|
///
|
||||||
|
/// Example: mirror_for_arch(ubuntu, riscv64) => the ports mirror
|
||||||
|
pub fn mirror_for_arch(dist: &str, arch: &str) -> Result<&'static Mirror, Box<dyn Error>> {
|
||||||
|
let data = dist_data(dist)?;
|
||||||
|
if let Some(primary) = data.mirrors.get(PRIMARY_MIRROR)
|
||||||
|
&& primary.serves(arch)
|
||||||
|
{
|
||||||
|
return Ok(primary);
|
||||||
|
}
|
||||||
|
let mut others: Vec<&String> = data
|
||||||
|
.mirrors
|
||||||
|
.keys()
|
||||||
|
.filter(|name| name.as_str() != PRIMARY_MIRROR)
|
||||||
|
.collect();
|
||||||
|
others.sort();
|
||||||
|
others
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|name| data.mirrors.get(name))
|
||||||
|
.find(|mirror| mirror.serves(arch))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"No mirror of '{dist}' serves the '{arch}' architecture. Supported \
|
||||||
|
distributions are: {}.",
|
||||||
supported_dists().join(", ")
|
supported_dists().join(", ")
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Host part of an apt-source URL: everything after the `://` scheme up
|
||||||
|
/// to the first `/` (a `:port` suffix stripped). URLs without a scheme
|
||||||
|
/// yield their leading segment.
|
||||||
|
fn url_host(url: &str) -> &str {
|
||||||
|
let after_scheme = url.split_once("://").map_or(url, |(_, rest)| rest);
|
||||||
|
let authority = after_scheme.split('/').next().unwrap_or(after_scheme);
|
||||||
|
authority
|
||||||
|
.split_once(':')
|
||||||
|
.map_or(authority, |(host, _)| host)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the host of `uri` is the host of `mirror_url` or a subdomain
|
||||||
|
/// of it: the old substring checks (`uri.contains("archive.ubuntu.com")`)
|
||||||
|
/// intentionally matched the country mirrors fronting each archive host
|
||||||
|
/// (`fr.archive.ubuntu.com`), and an exact host comparison would have
|
||||||
|
/// dropped them. The leading dot of the suffix keeps look-alike hosts
|
||||||
|
/// (`notarchive.ubuntu.com`) out.
|
||||||
|
fn uri_matches_url_host(uri: &str, mirror_url: &str) -> bool {
|
||||||
|
let host = url_host(mirror_url);
|
||||||
|
let uri_host = url_host(uri);
|
||||||
|
uri_host == host || uri_host.ends_with(&format!(".{host}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `uri` points at `mirror` or its security sibling: the URI's
|
||||||
|
/// host is the mirror's (or the sibling's) host or a subdomain of it (see
|
||||||
|
/// [`uri_matches_url_host`])
|
||||||
|
pub fn is_mirror_source(mirror: &Mirror, uri: &str) -> bool {
|
||||||
|
uri_matches_url_host(uri, &mirror.url)
|
||||||
|
|| mirror
|
||||||
|
.security_url
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|security| uri_matches_url_host(uri, security))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `uri` points at an official source of `dist` — one of its
|
||||||
|
/// archive mirrors or their security siblings — as opposed to a PPA or
|
||||||
|
/// another third-party repository. Unknown distributions match nothing.
|
||||||
|
pub fn is_official_source(dist: &str, uri: &str) -> bool {
|
||||||
|
DATA.dist.get(dist).is_some_and(|data| {
|
||||||
|
data.mirrors
|
||||||
|
.values()
|
||||||
|
.any(|mirror| is_mirror_source(mirror, uri))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Obtain the URLs for the archive keyrings of a distribution series
|
/// Obtain the URLs for the archive keyrings of a distribution series
|
||||||
///
|
///
|
||||||
/// For 'sid' and 'experimental', returns keyrings from the 3 latest releases
|
/// For 'sid' and 'experimental', returns keyrings from the 3 latest releases
|
||||||
@@ -564,6 +729,143 @@ mod tests {
|
|||||||
assert!(get_sections("not-a-distro").is_err());
|
assert!(get_sections("not-a-distro").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The primary mirror's URL is the former `base_url`, byte for byte:
|
||||||
|
/// the pull paths build their archive URLs from it.
|
||||||
|
#[test]
|
||||||
|
fn test_primary_mirror_url_is_the_base_url() {
|
||||||
|
assert_eq!(
|
||||||
|
get_base_url("ubuntu").unwrap(),
|
||||||
|
"https://archive.ubuntu.com/ubuntu"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_base_url("debian").unwrap(),
|
||||||
|
"https://deb.debian.org/debian"
|
||||||
|
);
|
||||||
|
assert!(get_base_url("not-a-distro").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mirror-per-architecture resolution: the local architectures come
|
||||||
|
/// from Ubuntu's primary mirror, the others from ports; Debian's `all`
|
||||||
|
/// sentinel makes its one mirror serve everything, including
|
||||||
|
/// architectures the data never lists.
|
||||||
|
#[test]
|
||||||
|
fn test_mirror_for_arch() {
|
||||||
|
assert_eq!(
|
||||||
|
mirror_for_arch("ubuntu", "amd64").unwrap().url,
|
||||||
|
"https://archive.ubuntu.com/ubuntu"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mirror_for_arch("ubuntu", "riscv64").unwrap().url,
|
||||||
|
"http://ports.ubuntu.com/ubuntu-ports"
|
||||||
|
);
|
||||||
|
for arch in ["amd64", "riscv64", "brand-new"] {
|
||||||
|
assert_eq!(
|
||||||
|
mirror_for_arch("debian", arch).unwrap().url,
|
||||||
|
"https://deb.debian.org/debian",
|
||||||
|
"the `all` sentinel serves every architecture, including {arch}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// An architecture no Ubuntu mirror serves, and an unknown dist.
|
||||||
|
assert!(mirror_for_arch("ubuntu", "mips64el").is_err());
|
||||||
|
assert!(mirror_for_arch("not-a-distro", "amd64").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `archs` forms and their reading: the `all` sentinel serves
|
||||||
|
/// everything, an explicit list serves exactly its members, and any
|
||||||
|
/// other scalar serves nothing (a data bug a validation would have to
|
||||||
|
/// catch, hence the documented reading).
|
||||||
|
#[test]
|
||||||
|
fn test_mirror_archs_forms() {
|
||||||
|
let all: MirrorArchs = serde_yaml::from_str("all").unwrap();
|
||||||
|
assert!(all.serves("anything"));
|
||||||
|
let list: MirrorArchs = serde_yaml::from_str("[amd64, i386]").unwrap();
|
||||||
|
assert!(list.serves("amd64"));
|
||||||
|
assert!(!list.serves("arm64"));
|
||||||
|
let typo: MirrorArchs = serde_yaml::from_str("every").unwrap();
|
||||||
|
assert!(!typo.serves("amd64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Official-source matching is host-based but keeps matching the
|
||||||
|
/// country mirrors the old substring checks matched (`fr.archive.
|
||||||
|
/// ubuntu.com`): equality or a `.{host}` suffix, never a bare
|
||||||
|
/// substring — `notarchive.ubuntu.com` must not match.
|
||||||
|
#[test]
|
||||||
|
fn test_is_official_source_matches_country_mirrors_only() {
|
||||||
|
for uri in [
|
||||||
|
"https://archive.ubuntu.com/ubuntu",
|
||||||
|
"http://security.ubuntu.com/ubuntu",
|
||||||
|
"http://ports.ubuntu.com/ubuntu-ports",
|
||||||
|
// Country mirrors front the same archives.
|
||||||
|
"http://fr.archive.ubuntu.com/ubuntu",
|
||||||
|
"https://de.security.ubuntu.com/ubuntu",
|
||||||
|
] {
|
||||||
|
assert!(is_official_source("ubuntu", uri), "{uri}");
|
||||||
|
}
|
||||||
|
for uri in [
|
||||||
|
"https://deb.debian.org/debian",
|
||||||
|
"https://ppa.launchpadcontent.net/user/ppa/ubuntu",
|
||||||
|
"http://notarchive.ubuntu.com/ubuntu",
|
||||||
|
"http://archive.ubuntu.com.evil.example/ubuntu",
|
||||||
|
] {
|
||||||
|
assert!(!is_official_source("ubuntu", uri), "{uri}");
|
||||||
|
}
|
||||||
|
// Debian: its own mirror matches, Ubuntu's mirrors do not, and an
|
||||||
|
// unknown dist matches nothing.
|
||||||
|
assert!(is_official_source(
|
||||||
|
"debian",
|
||||||
|
"https://deb.debian.org/debian"
|
||||||
|
));
|
||||||
|
assert!(!is_official_source(
|
||||||
|
"debian",
|
||||||
|
"http://security.ubuntu.com/ubuntu"
|
||||||
|
));
|
||||||
|
assert!(!is_official_source(
|
||||||
|
"not-a-distro",
|
||||||
|
"https://deb.debian.org/debian"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dist-level defaults the build paths read: components,
|
||||||
|
/// cross-build pockets (deliberately not the pull search order) and
|
||||||
|
/// vendor build profiles.
|
||||||
|
#[test]
|
||||||
|
fn test_dist_components_cross_pockets_and_build_profiles() {
|
||||||
|
assert_eq!(
|
||||||
|
get_dist_components("ubuntu").unwrap(),
|
||||||
|
vec!["main", "restricted", "universe", "multiverse"]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
get_dist_components("debian")
|
||||||
|
.unwrap()
|
||||||
|
.contains(&"non-free-firmware".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
for dist in ["debian", "ubuntu"] {
|
||||||
|
assert_eq!(
|
||||||
|
get_cross_pockets(dist).unwrap(),
|
||||||
|
vec!["updates", "backports", "security"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Not the pull search order: no 'proposed', no empty main pocket.
|
||||||
|
let cross = get_cross_pockets("ubuntu").unwrap();
|
||||||
|
assert!(!cross.contains(&"proposed".to_string()));
|
||||||
|
assert!(!cross.contains(&"".to_string()));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_build_profiles("ubuntu").unwrap(),
|
||||||
|
vec!["derivative.ubuntu", "noudeb"]
|
||||||
|
);
|
||||||
|
assert!(get_build_profiles("debian").unwrap().is_empty());
|
||||||
|
|
||||||
|
for getter in [
|
||||||
|
get_dist_components as fn(&str) -> Result<Vec<String>, Box<dyn Error>>,
|
||||||
|
get_cross_pockets as fn(&str) -> Result<Vec<String>, Box<dyn Error>>,
|
||||||
|
get_build_profiles as fn(&str) -> Result<Vec<String>, Box<dyn Error>>,
|
||||||
|
] {
|
||||||
|
assert!(getter("not-a-distro").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_series_csv_malformed_rows() {
|
fn test_parse_series_csv_malformed_rows() {
|
||||||
// A short row (missing 'codename') is skipped, a row with an invalid
|
// A short row (missing 'codename') is skipped, a row with an invalid
|
||||||
|
|||||||
+87
-10
@@ -23,13 +23,69 @@ use std::path::Path;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::data::embed_data;
|
||||||
use crate::put::target::UploadTarget;
|
use crate::put::target::UploadTarget;
|
||||||
|
|
||||||
/// Git configuration key holding the Launchpad account name
|
/// Git configuration key holding the Launchpad account name
|
||||||
const LP_USER_KEY: &str = "lp.user";
|
const LP_USER_KEY: &str = "lp.user";
|
||||||
|
|
||||||
|
/// Launchpad service endpoints, loaded from the bundled `launchpad.yml`
|
||||||
|
/// data file (same pattern as `distro_info.yml`): static endpoints that
|
||||||
|
/// change with Launchpad, not with the code, are data — several of them
|
||||||
|
/// were previously duplicated across three modules.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LaunchpadData {
|
||||||
/// Base URL of the Launchpad REST API
|
/// Base URL of the Launchpad REST API
|
||||||
const API_BASE: &str = "https://api.launchpad.net/1.0";
|
api_base: String,
|
||||||
|
/// Host of the PPA SFTP upload server
|
||||||
|
ssh_host: String,
|
||||||
|
/// Port of the PPA SFTP upload server
|
||||||
|
ssh_port: u16,
|
||||||
|
/// Upload queue incoming directory template (`{owner}`/`{ppa}`)
|
||||||
|
incoming_template: String,
|
||||||
|
/// PPA package-content (apt repository) URL template
|
||||||
|
content_host_template: String,
|
||||||
|
/// Ubuntu source-package git web URL template (`{package}`)
|
||||||
|
git_web_template: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_data! {
|
||||||
|
static ref LAUNCHPAD_DATA: LaunchpadData = "../data/launchpad.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base URL of the Launchpad REST API
|
||||||
|
fn api_base() -> &'static str {
|
||||||
|
&LAUNCHPAD_DATA.api_base
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Host serving PPA package content, derived from the content-host
|
||||||
|
/// template so the URL builders and the URL parsers of PPA addresses
|
||||||
|
/// cannot drift apart
|
||||||
|
pub(crate) fn ppa_content_host() -> &'static str {
|
||||||
|
let template = LAUNCHPAD_DATA.content_host_template.as_str();
|
||||||
|
let after_scheme = template
|
||||||
|
.split_once("://")
|
||||||
|
.map_or(template, |(_, rest)| rest);
|
||||||
|
after_scheme.split('/').next().unwrap_or(after_scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base URL of the apt repository serving a PPA's packages
|
||||||
|
/// (e.g. `https://ppa.launchpadcontent.net/user/ppa/ubuntu`)
|
||||||
|
pub(crate) fn ppa_content_url(owner: &str, ppa: &str) -> String {
|
||||||
|
LAUNCHPAD_DATA
|
||||||
|
.content_host_template
|
||||||
|
.replace("{owner}", owner)
|
||||||
|
.replace("{ppa}", ppa)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL of the Launchpad git repository of an Ubuntu source package
|
||||||
|
/// (`git.launchpad.net/ubuntu/+source/<package>`), the preferred VCS of
|
||||||
|
/// Ubuntu packages
|
||||||
|
pub(crate) fn ubuntu_source_git_url(package: &str) -> String {
|
||||||
|
LAUNCHPAD_DATA
|
||||||
|
.git_web_template
|
||||||
|
.replace("{package}", package)
|
||||||
|
}
|
||||||
|
|
||||||
/// Page size (`ws.size`) asked from Launchpad collections. Launchpad
|
/// Page size (`ws.size`) asked from Launchpad collections. Launchpad
|
||||||
/// truncates collection answers at 75 entries by default and rejects
|
/// truncates collection answers at 75 entries by default and rejects
|
||||||
@@ -88,26 +144,30 @@ fn split_ppa(ppa: &str) -> Result<(String, String), String> {
|
|||||||
|
|
||||||
/// URL of the Launchpad API resource of a Launchpad account
|
/// URL of the Launchpad API resource of a Launchpad account
|
||||||
fn person_url(user: &str) -> String {
|
fn person_url(user: &str) -> String {
|
||||||
format!("{API_BASE}/~{user}")
|
format!("{}/~{user}", api_base())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// URL of the Launchpad API resource of a PPA (`~user/+archive/ubuntu/name`
|
/// URL of the Launchpad API resource of a PPA (`~user/+archive/ubuntu/name`
|
||||||
/// covers the default `ppa` archive and named archives alike)
|
/// covers the default `ppa` archive and named archives alike); shared by the
|
||||||
fn archive_url(user: &str, ppa: &str) -> String {
|
/// put-side pre-flight checks and the apt keyring's fingerprint lookup
|
||||||
format!("{API_BASE}/~{user}/+archive/ubuntu/{ppa}")
|
pub(crate) fn archive_url(user: &str, ppa: &str) -> String {
|
||||||
|
format!("{}/~{user}/+archive/ubuntu/{ppa}", api_base())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a `user/ppa_name` PPA argument into its upload target
|
/// Resolve a `user/ppa_name` PPA argument into its upload target (the
|
||||||
/// (`ppa.launchpad.net`, incoming `~user/ppa_name`), like dput-ng's
|
/// SFTP host and incoming template of `launchpad.yml`), like dput-ng's
|
||||||
/// `ppa:user/ppa` profile expansion.
|
/// `ppa:user/ppa` profile expansion.
|
||||||
pub fn ppa_target(ppa: &str) -> Result<UploadTarget, String> {
|
pub fn ppa_target(ppa: &str) -> Result<UploadTarget, String> {
|
||||||
let (user, name) = split_ppa(ppa)?;
|
let (user, name) = split_ppa(ppa)?;
|
||||||
|
|
||||||
Ok(UploadTarget {
|
Ok(UploadTarget {
|
||||||
fqdn: "ppa.launchpad.net".to_string(),
|
fqdn: LAUNCHPAD_DATA.ssh_host.clone(),
|
||||||
port: 22,
|
port: LAUNCHPAD_DATA.ssh_port,
|
||||||
login: None,
|
login: None,
|
||||||
incoming: format!("~{user}/{name}"),
|
incoming: LAUNCHPAD_DATA
|
||||||
|
.incoming_template
|
||||||
|
.replace("{owner}", &user)
|
||||||
|
.replace("{ppa}", &name),
|
||||||
label: format!("ppa:{ppa}"),
|
label: format!("ppa:{ppa}"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -424,6 +484,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The data-driven endpoint accessors build the same addresses the
|
||||||
|
/// former hardcoded constants carried (each verified against the live
|
||||||
|
/// service), and the content host is derived from the same template
|
||||||
|
/// the content URLs are built from
|
||||||
|
#[test]
|
||||||
|
fn data_driven_endpoints_match_the_service() {
|
||||||
|
assert_eq!(
|
||||||
|
ppa_content_url("vhaudiquet", "noctalia"),
|
||||||
|
"https://ppa.launchpadcontent.net/vhaudiquet/noctalia/ubuntu"
|
||||||
|
);
|
||||||
|
assert_eq!(ppa_content_host(), "ppa.launchpadcontent.net");
|
||||||
|
assert_eq!(
|
||||||
|
ubuntu_source_git_url("hello"),
|
||||||
|
"https://git.launchpad.net/ubuntu/+source/hello"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The API answer carries many unrelated fields; deserialization must
|
/// The API answer carries many unrelated fields; deserialization must
|
||||||
/// pick the relevant ones and tolerate a null `enabled`
|
/// pick the relevant ones and tolerate a null `enabled`
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ pub mod apt;
|
|||||||
pub mod build;
|
pub mod build;
|
||||||
/// Parse or edit a Debian changelog of a source package
|
/// Parse or edit a Debian changelog of a source package
|
||||||
pub mod changelog;
|
pub mod changelog;
|
||||||
|
/// Embedding convention for static reference data (`data/*.yml`), applied
|
||||||
|
/// by each owning module via the `embed_data!` macro
|
||||||
|
pub(crate) mod data;
|
||||||
/// Build a Debian package into a binary (.deb)
|
/// Build a Debian package into a binary (.deb)
|
||||||
pub mod deb;
|
pub mod deb;
|
||||||
/// Reusable Debian format primitives (control/deb822, checksums, versions,
|
/// Reusable Debian format primitives (control/deb822, checksums, versions,
|
||||||
|
|||||||
+11
-11
@@ -51,7 +51,7 @@ pub fn orig_tarball_path(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render every common `debian/` file of the package.
|
/// Render every common `debian/` file of the package.
|
||||||
pub fn files(opts: &NewOptions, template: &dyn Template) -> Vec<OutputFile> {
|
pub fn files(opts: &NewOptions, template: &Template) -> Vec<OutputFile> {
|
||||||
let mut files = vec![
|
let mut files = vec![
|
||||||
source_format(opts),
|
source_format(opts),
|
||||||
changelog(opts),
|
changelog(opts),
|
||||||
@@ -195,7 +195,7 @@ fn render_continuation_text(text: &str) -> String {
|
|||||||
/// comes from the template (`all` for shell/empty), and a non-empty
|
/// comes from the template (`all` for shell/empty), and a non-empty
|
||||||
/// `opts.depends` (the empty/metapackage flavor) lands in the binary
|
/// `opts.depends` (the empty/metapackage flavor) lands in the binary
|
||||||
/// stanza's `Depends` field.
|
/// stanza's `Depends` field.
|
||||||
fn control(opts: &NewOptions, template: &dyn Template) -> OutputFile {
|
fn control(opts: &NewOptions, template: &Template) -> OutputFile {
|
||||||
let mut control = String::new();
|
let mut control = String::new();
|
||||||
|
|
||||||
// Source stanza.
|
// Source stanza.
|
||||||
@@ -238,7 +238,7 @@ fn control(opts: &NewOptions, template: &dyn Template) -> OutputFile {
|
|||||||
/// `debian/rules`: the shebang and `%:` target whose recipe is the
|
/// `debian/rules`: the shebang and `%:` target whose recipe is the
|
||||||
/// template's dh line (plus the template's extra overrides, when any),
|
/// template's dh line (plus the template's extra overrides, when any),
|
||||||
/// written with the executable bit.
|
/// written with the executable bit.
|
||||||
fn rules(opts: &NewOptions, template: &dyn Template) -> OutputFile {
|
fn rules(opts: &NewOptions, template: &Template) -> OutputFile {
|
||||||
let mut contents = format!("#!/usr/bin/make -f\n%:\n\t{}\n", template.rules_dh_line());
|
let mut contents = format!("#!/usr/bin/make -f\n%:\n\t{}\n", template.rules_dh_line());
|
||||||
let extra = template.rules_extra(opts);
|
let extra = template.rules_extra(opts);
|
||||||
if !extra.is_empty() {
|
if !extra.is_empty() {
|
||||||
@@ -533,7 +533,7 @@ mod tests {
|
|||||||
fn opts() -> NewOptions {
|
fn opts() -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Shell,
|
template: TemplateId::SHELL,
|
||||||
source_dir: SourceDir::Skeleton,
|
source_dir: SourceDir::Skeleton,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -559,7 +559,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn source_format_and_local_options() {
|
fn source_format_and_local_options() {
|
||||||
let o = opts();
|
let o = opts();
|
||||||
let files = super::files(&o, crate::new::templates::get(TemplateId::Shell).unwrap());
|
let files = super::files(&o, crate::new::templates::get(TemplateId::SHELL).unwrap());
|
||||||
let find = |path: &str| {
|
let find = |path: &str| {
|
||||||
files
|
files
|
||||||
.iter()
|
.iter()
|
||||||
@@ -582,7 +582,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let files = super::files(
|
let files = super::files(
|
||||||
&native,
|
&native,
|
||||||
crate::new::templates::get(TemplateId::Shell).unwrap(),
|
crate::new::templates::get(TemplateId::SHELL).unwrap(),
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
files
|
files
|
||||||
@@ -642,7 +642,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn control_rendering_and_parse() {
|
fn control_rendering_and_parse() {
|
||||||
let o = opts();
|
let o = opts();
|
||||||
let control = super::control(&o, crate::new::templates::get(TemplateId::Shell).unwrap());
|
let control = super::control(&o, crate::new::templates::get(TemplateId::SHELL).unwrap());
|
||||||
|
|
||||||
// RFC822 continuation: first dep on the field line, the rest indented.
|
// RFC822 continuation: first dep on the field line, the rest indented.
|
||||||
assert!(
|
assert!(
|
||||||
@@ -679,7 +679,7 @@ mod tests {
|
|||||||
homepage: None,
|
homepage: None,
|
||||||
..opts()
|
..opts()
|
||||||
};
|
};
|
||||||
let control = super::control(&o, crate::new::templates::get(TemplateId::Shell).unwrap());
|
let control = super::control(&o, crate::new::templates::get(TemplateId::SHELL).unwrap());
|
||||||
assert!(!control.contents.contains("Homepage:"));
|
assert!(!control.contents.contains("Homepage:"));
|
||||||
let parsed = crate::debian::ControlInfo::parse_content(&control.contents).unwrap();
|
let parsed = crate::debian::ControlInfo::parse_content(&control.contents).unwrap();
|
||||||
assert!(parsed.source.get("Homepage").is_none());
|
assert!(parsed.source.get("Homepage").is_none());
|
||||||
@@ -688,7 +688,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rules_is_executable_minimal_makefile() {
|
fn rules_is_executable_minimal_makefile() {
|
||||||
let o = opts();
|
let o = opts();
|
||||||
let rules = super::rules(&o, crate::new::templates::get(TemplateId::Shell).unwrap());
|
let rules = super::rules(&o, crate::new::templates::get(TemplateId::SHELL).unwrap());
|
||||||
assert!(rules.executable);
|
assert!(rules.executable);
|
||||||
assert_eq!(rules.contents, "#!/usr/bin/make -f\n%:\n\tdh $@\n");
|
assert_eq!(rules.contents, "#!/usr/bin/make -f\n%:\n\tdh $@\n");
|
||||||
}
|
}
|
||||||
@@ -701,7 +701,7 @@ mod tests {
|
|||||||
"version=4\nhttps://github.com/example/mytool/releases .*/v?@ANY_VERSION@\\.tar\\.gz\n"
|
"version=4\nhttps://github.com/example/mytool/releases .*/v?@ANY_VERSION@\\.tar\\.gz\n"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
let files = super::files(&o, crate::new::templates::get(TemplateId::Shell).unwrap());
|
let files = super::files(&o, crate::new::templates::get(TemplateId::SHELL).unwrap());
|
||||||
let find = |path: &str| {
|
let find = |path: &str| {
|
||||||
files
|
files
|
||||||
.iter()
|
.iter()
|
||||||
@@ -731,7 +731,7 @@ mod tests {
|
|||||||
// Without the extras none of the files are rendered.
|
// Without the extras none of the files are rendered.
|
||||||
let plain = super::files(
|
let plain = super::files(
|
||||||
&opts(),
|
&opts(),
|
||||||
crate::new::templates::get(TemplateId::Shell).unwrap(),
|
crate::new::templates::get(TemplateId::SHELL).unwrap(),
|
||||||
);
|
);
|
||||||
assert!(!plain.iter().any(|f| f.path.starts_with("debian/tests")));
|
assert!(!plain.iter().any(|f| f.path.starts_with("debian/tests")));
|
||||||
assert!(!plain.iter().any(|f| f.path == "debian/watch"));
|
assert!(!plain.iter().any(|f| f.path == "debian/watch"));
|
||||||
|
|||||||
+61
-91
@@ -4,13 +4,16 @@
|
|||||||
//! The rule set is deliberately simple and table-driven (highest precedence
|
//! The rule set is deliberately simple and table-driven (highest precedence
|
||||||
//! first):
|
//! first):
|
||||||
//!
|
//!
|
||||||
//! 1. well-known build-system marker files at the top level of the
|
//! 1. the `detect.files` marker files declared by the template manifests
|
||||||
//! directory (`Cargo.toml`, `pyproject.toml`/`setup.py`/`setup.cfg`,
|
//! (`data/templates/<id>/manifest.yml`, in registry order: `Cargo.toml`,
|
||||||
//! `meson.build`, `CMakeLists.txt`, `configure.ac`, `go.mod`,
|
//! `pyproject.toml`/`setup.py`/`setup.cfg`, `meson.build`,
|
||||||
//! `Makefile`) — more than one distinct template matching is
|
//! `CMakeLists.txt`, `configure.ac`, `go.mod`, `Makefile`) looked for at
|
||||||
//! [`Detection::Ambiguous`],
|
//! the top level of the directory — more than one distinct template
|
||||||
|
//! matching is [`Detection::Ambiguous`]; templates without markers
|
||||||
|
//! (shell: the single-script heuristic below; empty: never detected)
|
||||||
|
//! declare none,
|
||||||
//! 2. otherwise, exactly one top-level script (a `*.sh` file, or a file
|
//! 2. otherwise, exactly one top-level script (a `*.sh` file, or a file
|
||||||
//! whose first line is a `#!` shebang) → [`TemplateId::Shell`],
|
//! whose first line is a `#!` shebang) → [`TemplateId::SHELL`],
|
||||||
//! several scripts or none → nothing,
|
//! several scripts or none → nothing,
|
||||||
//! 3. otherwise [`Detection::Empty`].
|
//! 3. otherwise [`Detection::Empty`].
|
||||||
//!
|
//!
|
||||||
@@ -23,7 +26,9 @@ use std::path::Path;
|
|||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
use super::licenses;
|
||||||
use super::options::TemplateId;
|
use super::options::TemplateId;
|
||||||
|
use super::templates;
|
||||||
|
|
||||||
/// Outcome of the detection.
|
/// Outcome of the detection.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -37,26 +42,18 @@ pub enum Detection {
|
|||||||
Empty,
|
Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker files per template, in precedence order (see the module docs).
|
/// Detect the template matching the project in `dir`: the manifests'
|
||||||
const MARKERS: [(TemplateId, &[&str]); 7] = [
|
/// marker files in registry order (the detection priority), then the
|
||||||
(TemplateId::Rust, &["Cargo.toml"]),
|
/// shell single-script heuristic.
|
||||||
(
|
|
||||||
TemplateId::Python,
|
|
||||||
&["pyproject.toml", "setup.py", "setup.cfg"],
|
|
||||||
),
|
|
||||||
(TemplateId::Meson, &["meson.build"]),
|
|
||||||
(TemplateId::Cmake, &["CMakeLists.txt"]),
|
|
||||||
(TemplateId::Autotools, &["configure.ac"]),
|
|
||||||
(TemplateId::Go, &["go.mod"]),
|
|
||||||
(TemplateId::Makefile, &["Makefile"]),
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Detect the template matching the project in `dir`.
|
|
||||||
pub fn detect(dir: &Path) -> Detection {
|
pub fn detect(dir: &Path) -> Detection {
|
||||||
let mut hits: Vec<TemplateId> = Vec::new();
|
let mut hits: Vec<TemplateId> = Vec::new();
|
||||||
for (id, markers) in MARKERS {
|
for template in templates::all() {
|
||||||
if markers.iter().any(|marker| dir.join(marker).exists()) && !hits.contains(&id) {
|
let markers = template.detect_files();
|
||||||
hits.push(id);
|
if !markers.is_empty()
|
||||||
|
&& markers.iter().any(|marker| dir.join(marker).exists())
|
||||||
|
&& !hits.contains(&template.id())
|
||||||
|
{
|
||||||
|
hits.push(template.id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +64,7 @@ pub fn detect(dir: &Path) -> Detection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if single_script(dir).is_some() {
|
if single_script(dir).is_some() {
|
||||||
Detection::Single(TemplateId::Shell)
|
Detection::Single(TemplateId::SHELL)
|
||||||
} else {
|
} else {
|
||||||
Detection::Empty
|
Detection::Empty
|
||||||
}
|
}
|
||||||
@@ -109,23 +106,14 @@ fn has_shebang(path: &Path) -> bool {
|
|||||||
content.starts_with(b"#!")
|
content.starts_with(b"#!")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// License files looked at by [`sniff_license`], in preference order.
|
|
||||||
const LICENSE_FILES: [&str; 5] = [
|
|
||||||
"LICENSE",
|
|
||||||
"LICENSE.md",
|
|
||||||
"LICENSE.txt",
|
|
||||||
"COPYING",
|
|
||||||
"COPYING.txt",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Sniff the license of the project in `dir` from its `LICENSE`/`COPYING`
|
/// Sniff the license of the project in `dir` from its `LICENSE`/`COPYING`
|
||||||
/// file: an `SPDX-License-Identifier:` line wins, otherwise the text is
|
/// file: an `SPDX-License-Identifier:` line wins, otherwise the text is
|
||||||
/// matched against a short list of recognizable licenses (MIT, BSD-2/3,
|
/// matched against the marker sets of the bundled license table
|
||||||
/// Apache-2.0, GPL-2/3, LGPL-2.1/3, ISC). `None` when no license file
|
/// (`data/licenses.yml`: MIT, BSD-2/3, Apache-2.0, GPL-2/3, LGPL-2.1/3,
|
||||||
/// exists or nothing recognizable is found.
|
/// ISC). `None` when no license file exists or nothing recognizable is
|
||||||
|
/// found.
|
||||||
pub fn sniff_license(dir: &Path) -> Option<String> {
|
pub fn sniff_license(dir: &Path) -> Option<String> {
|
||||||
let content = LICENSE_FILES
|
let content = licenses::detect_files()
|
||||||
.iter()
|
|
||||||
.find_map(|name| std::fs::read_to_string(dir.join(name)).ok())
|
.find_map(|name| std::fs::read_to_string(dir.join(name)).ok())
|
||||||
// Case variants and suffixes (LICENSE-MIT, LICENCE, cpYING…): the
|
// Case variants and suffixes (LICENSE-MIT, LICENCE, cpYING…): the
|
||||||
// first top-level file whose name looks like a license notice.
|
// first top-level file whose name looks like a license notice.
|
||||||
@@ -166,43 +154,10 @@ pub fn sniff_license(dir: &Path) -> Option<String> {
|
|||||||
return Some(id);
|
return Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The recognizable-license markers live in the bundled table; the
|
||||||
|
// LICENSE_TEXTS test below is their behavioral lock.
|
||||||
let text = content.to_ascii_lowercase();
|
let text = content.to_ascii_lowercase();
|
||||||
if text.contains("apache license") && text.contains("version 2") {
|
licenses::detect_from_text(&text).map(str::to_string)
|
||||||
return Some("Apache-2.0".to_string());
|
|
||||||
}
|
|
||||||
if text.contains("lesser general public license") {
|
|
||||||
return if text.contains("version 3") && !text.contains("version 2.1") {
|
|
||||||
Some("LGPL-3.0+".to_string())
|
|
||||||
} else {
|
|
||||||
Some("LGPL-2.1+".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if text.contains("general public license") {
|
|
||||||
return if text.contains("version 3") {
|
|
||||||
Some("GPL-3.0+".to_string())
|
|
||||||
} else {
|
|
||||||
Some("GPL-2.0+".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if text.contains("mit license") || text.contains("permission is hereby granted, free of charge")
|
|
||||||
{
|
|
||||||
return Some("MIT".to_string());
|
|
||||||
}
|
|
||||||
if text.contains("isc license")
|
|
||||||
|| text.contains("permission to use, copy, modify, and/or distribute this software")
|
|
||||||
{
|
|
||||||
return Some("ISC".to_string());
|
|
||||||
}
|
|
||||||
if text.contains("redistribution and use in source and binary forms") {
|
|
||||||
// The third clause (name endorsement) is what sets BSD-3 apart
|
|
||||||
// from BSD-2.
|
|
||||||
return if text.contains("endorse or promote") {
|
|
||||||
Some("BSD-3-Clause".to_string())
|
|
||||||
} else {
|
|
||||||
Some("BSD-2-Clause".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -217,15 +172,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn marker_files_map_to_templates() {
|
fn marker_files_map_to_templates() {
|
||||||
let cases = [
|
let cases = [
|
||||||
("Cargo.toml", TemplateId::Rust),
|
("Cargo.toml", TemplateId::RUST),
|
||||||
("pyproject.toml", TemplateId::Python),
|
("pyproject.toml", TemplateId::PYTHON),
|
||||||
("setup.py", TemplateId::Python),
|
("setup.py", TemplateId::PYTHON),
|
||||||
("setup.cfg", TemplateId::Python),
|
("setup.cfg", TemplateId::PYTHON),
|
||||||
("meson.build", TemplateId::Meson),
|
("meson.build", TemplateId::MESON),
|
||||||
("CMakeLists.txt", TemplateId::Cmake),
|
("CMakeLists.txt", TemplateId::CMAKE),
|
||||||
("configure.ac", TemplateId::Autotools),
|
("configure.ac", TemplateId::AUTOTOOLS),
|
||||||
("go.mod", TemplateId::Go),
|
("go.mod", TemplateId::GO),
|
||||||
("Makefile", TemplateId::Makefile),
|
("Makefile", TemplateId::MAKEFILE),
|
||||||
];
|
];
|
||||||
for (marker, expected) in cases {
|
for (marker, expected) in cases {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
@@ -241,21 +196,21 @@ mod tests {
|
|||||||
touch(dir.path(), "Makefile");
|
touch(dir.path(), "Makefile");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
detect(dir.path()),
|
detect(dir.path()),
|
||||||
Detection::Ambiguous(vec![TemplateId::Rust, TemplateId::Makefile])
|
Detection::Ambiguous(vec![TemplateId::RUST, TemplateId::MAKEFILE])
|
||||||
);
|
);
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
touch(dir.path(), "pyproject.toml");
|
touch(dir.path(), "pyproject.toml");
|
||||||
touch(dir.path(), "setup.py");
|
touch(dir.path(), "setup.py");
|
||||||
// Both markers map to the same template: one hit, not ambiguous.
|
// Both markers map to the same template: one hit, not ambiguous.
|
||||||
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::Python));
|
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::PYTHON));
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
touch(dir.path(), "meson.build");
|
touch(dir.path(), "meson.build");
|
||||||
touch(dir.path(), "CMakeLists.txt");
|
touch(dir.path(), "CMakeLists.txt");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
detect(dir.path()),
|
detect(dir.path()),
|
||||||
Detection::Ambiguous(vec![TemplateId::Meson, TemplateId::Cmake])
|
Detection::Ambiguous(vec![TemplateId::MESON, TemplateId::CMAKE])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,12 +219,12 @@ mod tests {
|
|||||||
// .sh extension.
|
// .sh extension.
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
touch(dir.path(), "run.sh");
|
touch(dir.path(), "run.sh");
|
||||||
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::Shell));
|
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::SHELL));
|
||||||
|
|
||||||
// Shebang without extension.
|
// Shebang without extension.
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
std::fs::write(dir.path().join("run"), "#!/usr/bin/env python3\n").unwrap();
|
std::fs::write(dir.path().join("run"), "#!/usr/bin/env python3\n").unwrap();
|
||||||
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::Shell));
|
assert_eq!(detect(dir.path()), Detection::Single(TemplateId::SHELL));
|
||||||
|
|
||||||
// Two scripts: not exactly one, nothing recognized.
|
// Two scripts: not exactly one, nothing recognized.
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
@@ -302,8 +257,10 @@ mod tests {
|
|||||||
assert_eq!(detect(dir.path()), Detection::Empty);
|
assert_eq!(detect(dir.path()), Detection::Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Distinctive (shortened) excerpts of the recognizable license texts.
|
/// Distinctive (shortened) excerpts of the recognizable license texts:
|
||||||
const LICENSE_TEXTS: [(&str, &str); 9] = [
|
/// the behavioral lock of the marker sets in `data/licenses.yml` — a
|
||||||
|
/// bad marker edit fails here, not on real packages.
|
||||||
|
const LICENSE_TEXTS: [(&str, &str); 11] = [
|
||||||
(
|
(
|
||||||
"MIT",
|
"MIT",
|
||||||
"MIT License\n\nPermission is hereby granted, free of charge, to any person",
|
"MIT License\n\nPermission is hereby granted, free of charge, to any person",
|
||||||
@@ -337,6 +294,19 @@ mod tests {
|
|||||||
"ISC",
|
"ISC",
|
||||||
"ISC License\nPermission to use, copy, modify, and/or distribute this software",
|
"ISC License\nPermission to use, copy, modify, and/or distribute this software",
|
||||||
),
|
),
|
||||||
|
// Dual-licensed preamble: the GPL reference outranks the MIT
|
||||||
|
// boilerplate, like the old hardcoded cascade decided.
|
||||||
|
(
|
||||||
|
"GPL-2.0+",
|
||||||
|
"MIT License\n\nAlternatively, under the terms of the GNU General Public License,\
|
||||||
|
\nversion 2 of the License.",
|
||||||
|
),
|
||||||
|
// LGPL text naming both versions: the 2.1 wording wins.
|
||||||
|
(
|
||||||
|
"LGPL-2.1+",
|
||||||
|
"GNU LESSER GENERAL PUBLIC LICENSE\nVersion 2.1, February 1999\n\
|
||||||
|
This is version 2.1; version 3 is available separately.",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
//! License reference data for `pkh new`, bundled as `data/licenses.yml`.
|
||||||
|
//!
|
||||||
|
//! The license knowledge of the scaffolder used to live in three places —
|
||||||
|
//! the wizard menu (`questions.rs`), the parse/SPDX mapping (`options.rs`)
|
||||||
|
//! and the license-file sniffing inputs (`detect.rs`) — kept in sync by
|
||||||
|
//! comments only. It is one table now: [`entries`] carries each curated
|
||||||
|
//! license's menu label, accepted spellings and detection markers,
|
||||||
|
//! [`detect_files`] the candidate license file names and [`url_template`]
|
||||||
|
//! the SPDX page URL template, so the lists cannot drift apart and adding
|
||||||
|
//! a license is a YAML entry.
|
||||||
|
//!
|
||||||
|
//! The `License` enum stays in `options.rs` (`NewOptions` and the template
|
||||||
|
//! rendering match on its variants); the enum's variants and the table's
|
||||||
|
//! ids are locked together by a consistency test there. The free-text
|
||||||
|
//! "Other (enter a SPDX identifier)" wizard entry is UX, not data, and
|
||||||
|
//! stays in Rust — like `License::Custom`'s code-driven parse path.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::data::embed_data;
|
||||||
|
|
||||||
|
/// One marker set of the license-text sniff: the set matches when every
|
||||||
|
/// marker of `all` occurs in the lowercased license text and none of
|
||||||
|
/// `unless` does (all-markers-within-a-list, any-marker-list semantics).
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct DetectMarkerSet {
|
||||||
|
/// Substrings that must all occur in the license text
|
||||||
|
pub(crate) all: Vec<String>,
|
||||||
|
/// Substrings that must all be absent for the set to match
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) unless: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One curated license of the bundled table: everything `pkh new` knows
|
||||||
|
/// about it — identifier, menu label, accepted spellings and sniff markers
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct LicenseEntry {
|
||||||
|
/// SPDX identifier: written to `debian/copyright`, returned by the
|
||||||
|
/// license sniff, substituted into [`url_template`]
|
||||||
|
pub(crate) id: String,
|
||||||
|
/// Label offered by the wizard license menu
|
||||||
|
pub(crate) menu: String,
|
||||||
|
/// Inputs accepted by `License::parse` (case-insensitive); must
|
||||||
|
/// include the id itself
|
||||||
|
pub(crate) spellings: Vec<String>,
|
||||||
|
/// Marker sets of the license-text sniff (see [`detect_from_text`])
|
||||||
|
pub(crate) detect_markers: Vec<DetectMarkerSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bundled license table (`data/licenses.yml`): the curated licenses
|
||||||
|
/// plus the shared sniffing inputs and the SPDX URL template
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LicensesData {
|
||||||
|
/// SPDX license page URL template (`{id}` placeholder)
|
||||||
|
license_url_template: String,
|
||||||
|
/// Candidate license file names of the sniff, in preference order
|
||||||
|
detect_files: Vec<String>,
|
||||||
|
/// The curated licenses, in wizard-menu order
|
||||||
|
licenses: Vec<LicenseEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_data! {
|
||||||
|
static ref LICENSES_DATA: LicensesData = "../../data/licenses.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The curated license entries, in the order offered by the wizard menu
|
||||||
|
pub(crate) fn entries() -> &'static [LicenseEntry] {
|
||||||
|
&LICENSES_DATA.licenses
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The entry whose `spellings` contain `input` (case-insensitive): the
|
||||||
|
/// table lookup behind `License::parse`'s curated arm
|
||||||
|
pub(crate) fn entry_for_spelling(input: &str) -> Option<&'static LicenseEntry> {
|
||||||
|
entries().iter().find(|entry| {
|
||||||
|
entry
|
||||||
|
.spellings
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.eq_ignore_ascii_case(input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The candidate license file names looked at by the license sniff, in
|
||||||
|
/// preference order, shared by every license
|
||||||
|
pub(crate) fn detect_files() -> impl Iterator<Item = &'static str> {
|
||||||
|
LICENSES_DATA.detect_files.iter().map(String::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The SPDX identifier of the (lowercased) license `text`, `None` when
|
||||||
|
/// nothing is recognizable: the first entry (menu order) with a matching
|
||||||
|
/// marker set wins. The caller lowercases the text; the markers are
|
||||||
|
/// lowercase substrings.
|
||||||
|
pub(crate) fn detect_from_text(text: &str) -> Option<&'static str> {
|
||||||
|
entries()
|
||||||
|
.iter()
|
||||||
|
.find(|entry| markers_match(entry, text))
|
||||||
|
.map(|entry| entry.id.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether any marker set of `entry` matches `text` (see
|
||||||
|
/// [`DetectMarkerSet`] for the semantics)
|
||||||
|
fn markers_match(entry: &LicenseEntry, text: &str) -> bool {
|
||||||
|
entry.detect_markers.iter().any(|set| {
|
||||||
|
set.all.iter().all(|marker| text.contains(marker))
|
||||||
|
&& set.unless.iter().all(|marker| !text.contains(marker))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The SPDX license page URL template of the table, carrying the license
|
||||||
|
/// identifier as an `{id}` placeholder
|
||||||
|
pub(crate) fn url_template() -> &'static str {
|
||||||
|
LICENSES_DATA.license_url_template.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Every entry is menu-ready: non-empty id/menu, at least one spelling
|
||||||
|
/// and one non-empty marker set, and no duplicate ids or menus (the
|
||||||
|
/// wizard select would silently shadow a duplicate menu label).
|
||||||
|
#[test]
|
||||||
|
fn entries_are_well_formed() {
|
||||||
|
assert!(!entries().is_empty());
|
||||||
|
let mut ids: Vec<&str> = Vec::new();
|
||||||
|
let mut menus: Vec<&str> = Vec::new();
|
||||||
|
for entry in entries() {
|
||||||
|
assert!(!entry.id.is_empty());
|
||||||
|
assert!(!entry.menu.is_empty());
|
||||||
|
assert!(!entry.spellings.is_empty());
|
||||||
|
assert!(
|
||||||
|
!entry.detect_markers.is_empty(),
|
||||||
|
"entry '{}' has no marker set",
|
||||||
|
entry.id
|
||||||
|
);
|
||||||
|
for set in &entry.detect_markers {
|
||||||
|
assert!(
|
||||||
|
!set.all.is_empty(),
|
||||||
|
"entry '{}' has an empty marker set",
|
||||||
|
entry.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ids.push(entry.id.as_str());
|
||||||
|
menus.push(entry.menu.as_str());
|
||||||
|
}
|
||||||
|
ids.sort_unstable();
|
||||||
|
ids.dedup();
|
||||||
|
menus.sort_unstable();
|
||||||
|
menus.dedup();
|
||||||
|
assert_eq!(ids.len(), entries().len(), "duplicate ids");
|
||||||
|
assert_eq!(menus.len(), entries().len(), "duplicate menus");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shared sniffing inputs and the URL template: the canonical file
|
||||||
|
/// names in preference order, and a template the spdx_url
|
||||||
|
/// substitution can render.
|
||||||
|
#[test]
|
||||||
|
fn sniff_inputs_and_url_template() {
|
||||||
|
let files: Vec<&str> = detect_files().collect();
|
||||||
|
assert!(!files.is_empty());
|
||||||
|
assert_eq!(files[0], "LICENSE");
|
||||||
|
assert!(files.contains(&"COPYING"));
|
||||||
|
assert!(url_template().starts_with("https://spdx.org/licenses/"));
|
||||||
|
assert!(url_template().contains("{id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The marker semantics on hand-built texts: every `all` marker must
|
||||||
|
/// occur, every `unless` marker must not, any set of an entry
|
||||||
|
/// suffices, and the sets are mutually exclusive across entries —
|
||||||
|
/// the LGPL/GPL substring relation and the multi-license texts land
|
||||||
|
/// on the same entry the old hardcoded cascade picked.
|
||||||
|
#[test]
|
||||||
|
fn marker_sets_keep_the_old_cascade_results() {
|
||||||
|
// Nothing recognizable.
|
||||||
|
assert_eq!(detect_from_text("do whatever you want"), None);
|
||||||
|
// Version discrimination of the GPL family.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu general public license\nversion 3"),
|
||||||
|
Some("GPL-3.0+")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu general public license\nversion 2, june 1991"),
|
||||||
|
Some("GPL-2.0+")
|
||||||
|
);
|
||||||
|
// "lesser general public license" contains "general public
|
||||||
|
// license": LGPL texts must stay on their entries.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu lesser general public license\nversion 3"),
|
||||||
|
Some("LGPL-3.0+")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu lesser general public license\nversion 2.1"),
|
||||||
|
Some("LGPL-2.1+")
|
||||||
|
);
|
||||||
|
// A text naming both versions is the 2.1 wording (version 2.1
|
||||||
|
// wins), like the old unless-less branch pair did.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu lesser general public license\nversion 3, like version 2.1"),
|
||||||
|
Some("LGPL-2.1+")
|
||||||
|
);
|
||||||
|
// Multi-license texts: the GPL/Apache reference is the stronger
|
||||||
|
// one, so MIT does not steal them.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("mit license\nunder the gnu general public license, version 2"),
|
||||||
|
Some("GPL-2.0+")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text(
|
||||||
|
"permission is hereby granted, free of charge\ndual-licensed under the apache license version 2"
|
||||||
|
),
|
||||||
|
Some("Apache-2.0")
|
||||||
|
);
|
||||||
|
// BSD-2 vs BSD-3: the endorsement clause is the discriminator.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("redistribution and use in source and binary forms"),
|
||||||
|
Some("BSD-2-Clause")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text(
|
||||||
|
"redistribution and use in source and binary forms\nmay be used to endorse or promote"
|
||||||
|
),
|
||||||
|
Some("BSD-3-Clause")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-19
@@ -16,6 +16,8 @@
|
|||||||
pub mod debian;
|
pub mod debian;
|
||||||
pub mod detect;
|
pub mod detect;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
|
/// License reference data for the scaffolder (bundled `data/licenses.yml`)
|
||||||
|
pub(crate) mod licenses;
|
||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod orig;
|
pub mod orig;
|
||||||
pub mod origin;
|
pub mod origin;
|
||||||
@@ -179,7 +181,7 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<ScaffoldOutcome
|
|||||||
// mode, plus the template's own entries (rust: the vendored layout) in
|
// mode, plus the template's own entries (rust: the vendored layout) in
|
||||||
// every mode — missing ones appended, an existing file never
|
// every mode — missing ones appended, an existing file never
|
||||||
// overwritten.
|
// overwritten.
|
||||||
let template_gitignore = template.gitignore_entries(opts);
|
let template_gitignore = template.gitignore_entries();
|
||||||
let mut entries: Vec<&str> = Vec::new();
|
let mut entries: Vec<&str> = Vec::new();
|
||||||
let mut header = None;
|
let mut header = None;
|
||||||
if skeleton {
|
if skeleton {
|
||||||
@@ -214,7 +216,7 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<ScaffoldOutcome
|
|||||||
if opts.source_format == SourceFormat::Quilt {
|
if opts.source_format == SourceFormat::Quilt {
|
||||||
pb.set_message("Creating orig tarball");
|
pb.set_message("Creating orig tarball");
|
||||||
let vendored_rust =
|
let vendored_rust =
|
||||||
template.id() == options::TemplateId::Rust && orig::has_vendored_dir(&target);
|
template.id() == options::TemplateId::RUST && orig::has_vendored_dir(&target);
|
||||||
let created = orig::create_orig(
|
let created = orig::create_orig(
|
||||||
&target,
|
&target,
|
||||||
&opts.name,
|
&opts.name,
|
||||||
@@ -347,7 +349,7 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
scaffold_in(
|
scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton),
|
opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -421,7 +423,7 @@ mod tests {
|
|||||||
#[serial]
|
#[serial]
|
||||||
fn scaffold_skeleton_forced_quilt_snapshots_the_tree() {
|
fn scaffold_skeleton_forced_quilt_snapshots_the_tree() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let mut o = opts(TemplateId::Shell, "mytool", SourceDir::Skeleton);
|
let mut o = opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton);
|
||||||
o.source_format = SourceFormat::Quilt;
|
o.source_format = SourceFormat::Quilt;
|
||||||
o.orig = Some(OrigOrigin::Snapshot);
|
o.orig = Some(OrigOrigin::Snapshot);
|
||||||
scaffold_in(dir.path(), o).unwrap();
|
scaffold_in(dir.path(), o).unwrap();
|
||||||
@@ -469,7 +471,7 @@ mod tests {
|
|||||||
fn scaffold_empty_base_and_metapackage_flavors() {
|
fn scaffold_empty_base_and_metapackage_flavors() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
// Metapackage flavor: non-empty depends.
|
// Metapackage flavor: non-empty depends.
|
||||||
let mut o = opts(TemplateId::Empty, "metapkg", SourceDir::Skeleton);
|
let mut o = opts(TemplateId::EMPTY, "metapkg", SourceDir::Skeleton);
|
||||||
o.depends = vec!["hello".into(), "hello-data (>= 1.0)".into()];
|
o.depends = vec!["hello".into(), "hello-data (>= 1.0)".into()];
|
||||||
scaffold_in(dir.path(), o).unwrap();
|
scaffold_in(dir.path(), o).unwrap();
|
||||||
|
|
||||||
@@ -491,7 +493,7 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
scaffold_in(
|
scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Empty, "basepkg", SourceDir::Skeleton),
|
opts(TemplateId::EMPTY, "basepkg", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let control =
|
let control =
|
||||||
@@ -503,7 +505,7 @@ mod tests {
|
|||||||
#[serial]
|
#[serial]
|
||||||
fn scaffold_release_targets_series() {
|
fn scaffold_release_targets_series() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let mut o = opts(TemplateId::Empty, "released", SourceDir::Skeleton);
|
let mut o = opts(TemplateId::EMPTY, "released", SourceDir::Skeleton);
|
||||||
o.release = true;
|
o.release = true;
|
||||||
scaffold_in(dir.path(), o).unwrap();
|
scaffold_in(dir.path(), o).unwrap();
|
||||||
|
|
||||||
@@ -523,7 +525,7 @@ mod tests {
|
|||||||
let tree = dir.path().join("packdir");
|
let tree = dir.path().join("packdir");
|
||||||
std::fs::create_dir_all(&tree).unwrap();
|
std::fs::create_dir_all(&tree).unwrap();
|
||||||
std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").unwrap();
|
std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").unwrap();
|
||||||
scaffold_in(&tree, opts(TemplateId::Shell, "runtool", SourceDir::Here)).unwrap();
|
scaffold_in(&tree, opts(TemplateId::SHELL, "runtool", SourceDir::Here)).unwrap();
|
||||||
|
|
||||||
// debian/ lands directly in the directory; no skeleton file, no
|
// debian/ lands directly in the directory; no skeleton file, no
|
||||||
// root .gitignore (the shell template contributes none and the
|
// root .gitignore (the shell template contributes none and the
|
||||||
@@ -572,7 +574,7 @@ mod tests {
|
|||||||
std::fs::write(tree.join("debian/control"), "Source: mytool\n").unwrap();
|
std::fs::write(tree.join("debian/control"), "Source: mytool\n").unwrap();
|
||||||
let err = scaffold_in(
|
let err = scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton),
|
opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.to_string().contains("debian/control"), "{err}");
|
assert!(err.to_string().contains("debian/control"), "{err}");
|
||||||
@@ -583,7 +585,7 @@ mod tests {
|
|||||||
std::fs::write(dir.path().join("mytool/junk"), "x").unwrap();
|
std::fs::write(dir.path().join("mytool/junk"), "x").unwrap();
|
||||||
let err = scaffold_in(
|
let err = scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton),
|
opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.to_string().contains("not empty"), "{err}");
|
assert!(err.to_string().contains("not empty"), "{err}");
|
||||||
@@ -591,7 +593,7 @@ mod tests {
|
|||||||
// Existing orig tarball (quilt only): nothing gets written.
|
// Existing orig tarball (quilt only): nothing gets written.
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
std::fs::write(dir.path().join("mytool_0.1.0.orig.tar.xz"), b"old").unwrap();
|
std::fs::write(dir.path().join("mytool_0.1.0.orig.tar.xz"), b"old").unwrap();
|
||||||
let mut o = opts(TemplateId::Shell, "mytool", SourceDir::Skeleton);
|
let mut o = opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton);
|
||||||
o.source_format = SourceFormat::Quilt;
|
o.source_format = SourceFormat::Quilt;
|
||||||
o.orig = Some(OrigOrigin::Snapshot);
|
o.orig = Some(OrigOrigin::Snapshot);
|
||||||
let err = scaffold_in(dir.path(), o).unwrap_err();
|
let err = scaffold_in(dir.path(), o).unwrap_err();
|
||||||
@@ -603,7 +605,7 @@ mod tests {
|
|||||||
let err = scaffold_in(
|
let err = scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(
|
opts(
|
||||||
TemplateId::Shell,
|
TemplateId::SHELL,
|
||||||
"mytool",
|
"mytool",
|
||||||
SourceDir::Path(dir.path().join("missing")),
|
SourceDir::Path(dir.path().join("missing")),
|
||||||
),
|
),
|
||||||
@@ -620,7 +622,7 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
scaffold_in(
|
scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Shell, "nativepkg", SourceDir::Skeleton),
|
opts(TemplateId::SHELL, "nativepkg", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -647,7 +649,7 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let outcome = scaffold_in(
|
let outcome = scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Rust, "mytool", SourceDir::Skeleton),
|
opts(TemplateId::RUST, "mytool", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -709,7 +711,7 @@ mod tests {
|
|||||||
let tree = dir.path().join("packdir");
|
let tree = dir.path().join("packdir");
|
||||||
std::fs::create_dir_all(&tree).unwrap();
|
std::fs::create_dir_all(&tree).unwrap();
|
||||||
std::fs::write(tree.join(".gitignore"), "# my project\n*.log\n").unwrap();
|
std::fs::write(tree.join(".gitignore"), "# my project\n*.log\n").unwrap();
|
||||||
scaffold_in(&tree, opts(TemplateId::Rust, "mytool", SourceDir::Here)).unwrap();
|
scaffold_in(&tree, opts(TemplateId::RUST, "mytool", SourceDir::Here)).unwrap();
|
||||||
|
|
||||||
let gitignore = std::fs::read_to_string(tree.join(".gitignore")).unwrap();
|
let gitignore = std::fs::read_to_string(tree.join(".gitignore")).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -751,7 +753,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
std::fs::write(source.join("src/main.rs"), "fn main() {}\n").unwrap();
|
std::fs::write(source.join("src/main.rs"), "fn main() {}\n").unwrap();
|
||||||
|
|
||||||
let mut o = opts(TemplateId::Rust, "mytool", SourceDir::Path(source.clone()));
|
let mut o = opts(TemplateId::RUST, "mytool", SourceDir::Path(source.clone()));
|
||||||
o.source_format = SourceFormat::Quilt;
|
o.source_format = SourceFormat::Quilt;
|
||||||
o.orig = Some(OrigOrigin::Snapshot);
|
o.orig = Some(OrigOrigin::Snapshot);
|
||||||
let outcome = scaffold_in(dir.path(), o).unwrap();
|
let outcome = scaffold_in(dir.path(), o).unwrap();
|
||||||
@@ -846,7 +848,7 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
scaffold_in(
|
scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Python, "mytool", SourceDir::Skeleton),
|
opts(TemplateId::PYTHON, "mytool", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -878,7 +880,7 @@ mod tests {
|
|||||||
let tree = dir.path().join("mytool");
|
let tree = dir.path().join("mytool");
|
||||||
std::fs::create_dir_all(&tree).unwrap();
|
std::fs::create_dir_all(&tree).unwrap();
|
||||||
std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").unwrap();
|
std::fs::write(tree.join("run.sh"), "#!/bin/sh\necho hi\n").unwrap();
|
||||||
scaffold_in(&tree, opts(TemplateId::Shell, "mytool", SourceDir::Here)).unwrap();
|
scaffold_in(&tree, opts(TemplateId::SHELL, "mytool", SourceDir::Here)).unwrap();
|
||||||
|
|
||||||
let output = crate::build::run_source_build(
|
let output = crate::build::run_source_build(
|
||||||
&dir.path().join("mytool"),
|
&dir.path().join("mytool"),
|
||||||
@@ -907,7 +909,7 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
scaffold_in(
|
scaffold_in(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
opts(TemplateId::Shell, "mytool", SourceDir::Skeleton),
|
opts(TemplateId::SHELL, "mytool", SourceDir::Skeleton),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
+138
-99
@@ -15,77 +15,64 @@ use crate::debian::DebianVersion;
|
|||||||
use crate::debian::deps::{Deps, ParseOpts};
|
use crate::debian::deps::{Deps, ParseOpts};
|
||||||
use crate::distro_info;
|
use crate::distro_info;
|
||||||
use crate::new::detect::{self, Detection};
|
use crate::new::detect::{self, Detection};
|
||||||
|
use crate::new::licenses;
|
||||||
use crate::new::origin::{Forge, GitOrigin};
|
use crate::new::origin::{Forge, GitOrigin};
|
||||||
use crate::new::templates;
|
use crate::new::templates;
|
||||||
|
|
||||||
/// Build systems / project kinds `pkh new` knows about.
|
/// Build systems / project kinds `pkh new` knows about: a lightweight id
|
||||||
|
/// wrapping the stable CLI string (the `--lang` value).
|
||||||
///
|
///
|
||||||
/// The identifiers are stable CLI surface: `--lang` accepts every variant,
|
/// The templates themselves are defined by the per-template manifests
|
||||||
/// and every variant has a template implementation registered in
|
/// under `data/templates/<id>/` (see [`crate::new::templates`]): label,
|
||||||
/// [`crate::new::templates`].
|
/// detection markers, policy metadata and file bodies all live there, and
|
||||||
|
/// every id below must have a manifest registered — the registry is built
|
||||||
|
/// from exactly these constants and a consistency test keeps the two in
|
||||||
|
/// lockstep.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum TemplateId {
|
pub struct TemplateId(&'static str);
|
||||||
/// Rust project (`Cargo.toml`)
|
|
||||||
Rust,
|
|
||||||
/// Python project (`pyproject.toml` / `setup.py` / `setup.cfg`)
|
|
||||||
Python,
|
|
||||||
/// C/C++ with Meson (`meson.build`)
|
|
||||||
Meson,
|
|
||||||
/// C/C++ with CMake (`CMakeLists.txt`)
|
|
||||||
Cmake,
|
|
||||||
/// C/C++ with Autotools (`configure.ac`)
|
|
||||||
Autotools,
|
|
||||||
/// Go module (`go.mod`)
|
|
||||||
Go,
|
|
||||||
/// Shell script / single interpreted file
|
|
||||||
Shell,
|
|
||||||
/// Generic Makefile-based project
|
|
||||||
Makefile,
|
|
||||||
/// Metapackage / empty base (no build system)
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TemplateId {
|
impl TemplateId {
|
||||||
/// Every template id, in the order offered by the wizard language menu.
|
/// Rust project (`Cargo.toml`).
|
||||||
pub fn all() -> [TemplateId; 9] {
|
pub const RUST: TemplateId = TemplateId("rust");
|
||||||
[
|
/// Python project (`pyproject.toml` / `setup.py` / `setup.cfg`).
|
||||||
TemplateId::Rust,
|
pub const PYTHON: TemplateId = TemplateId("python");
|
||||||
TemplateId::Python,
|
/// C/C++ with Meson (`meson.build`).
|
||||||
TemplateId::Meson,
|
pub const MESON: TemplateId = TemplateId("meson");
|
||||||
TemplateId::Cmake,
|
/// C/C++ with CMake (`CMakeLists.txt`).
|
||||||
TemplateId::Autotools,
|
pub const CMAKE: TemplateId = TemplateId("cmake");
|
||||||
TemplateId::Go,
|
/// C/C++ with Autotools (`configure.ac`).
|
||||||
TemplateId::Shell,
|
pub const AUTOTOOLS: TemplateId = TemplateId("autotools");
|
||||||
TemplateId::Makefile,
|
/// Go module (`go.mod`).
|
||||||
TemplateId::Empty,
|
pub const GO: TemplateId = TemplateId("go");
|
||||||
]
|
/// Shell script / single interpreted file.
|
||||||
|
pub const SHELL: TemplateId = TemplateId("shell");
|
||||||
|
/// Generic Makefile-based project.
|
||||||
|
pub const MAKEFILE: TemplateId = TemplateId("makefile");
|
||||||
|
/// Metapackage / empty base (no build system).
|
||||||
|
pub const EMPTY: TemplateId = TemplateId("empty");
|
||||||
|
|
||||||
|
/// Every template id, in the order offered by the wizard language menu
|
||||||
|
/// (the registry order of the manifests).
|
||||||
|
pub fn all() -> &'static [TemplateId] {
|
||||||
|
templates::ids()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Canonical CLI identifier of this template.
|
/// Canonical CLI identifier of this template.
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
self.0
|
||||||
TemplateId::Rust => "rust",
|
|
||||||
TemplateId::Python => "python",
|
|
||||||
TemplateId::Meson => "meson",
|
|
||||||
TemplateId::Cmake => "cmake",
|
|
||||||
TemplateId::Autotools => "autotools",
|
|
||||||
TemplateId::Go => "go",
|
|
||||||
TemplateId::Shell => "shell",
|
|
||||||
TemplateId::Makefile => "makefile",
|
|
||||||
TemplateId::Empty => "empty",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a CLI identifier, accepting exactly the canonical spellings.
|
/// Parse a CLI identifier, accepting exactly the registered spellings.
|
||||||
pub fn parse(s: &str) -> Result<TemplateId, String> {
|
pub fn parse(s: &str) -> Result<TemplateId, String> {
|
||||||
TemplateId::all()
|
Self::all()
|
||||||
.into_iter()
|
.iter()
|
||||||
|
.copied()
|
||||||
.find(|id| id.as_str() == s)
|
.find(|id| id.as_str() == s)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
"Unknown language/template '{}'. Supported values are: {}.",
|
"Unknown language/template '{}'. Supported values are: {}.",
|
||||||
s,
|
s,
|
||||||
TemplateId::all()
|
Self::all()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|id| id.as_str())
|
.map(|id| id.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -95,26 +82,15 @@ impl TemplateId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Human-readable menu label of this template, as offered by the wizard
|
/// Human-readable menu label of this template, as offered by the wizard
|
||||||
/// language question (and reused in the summary screen).
|
/// language question (and reused in the summary screen): the
|
||||||
|
/// manifest's `label`.
|
||||||
pub fn display_name(&self) -> &'static str {
|
pub fn display_name(&self) -> &'static str {
|
||||||
match self {
|
templates::get(*self).map(|t| t.label()).unwrap_or(self.0)
|
||||||
TemplateId::Rust => "Rust (Cargo.toml)",
|
|
||||||
TemplateId::Python => "Python (pyproject.toml / setup.py)",
|
|
||||||
TemplateId::Meson => "C/C++ (Meson)",
|
|
||||||
TemplateId::Cmake => "C/C++ (CMake)",
|
|
||||||
TemplateId::Autotools => "C/C++ (Autotools)",
|
|
||||||
TemplateId::Go => "Go module",
|
|
||||||
TemplateId::Shell => "Shell script / single interpreted file",
|
|
||||||
TemplateId::Makefile => "Generic (Makefile)",
|
|
||||||
TemplateId::Empty => "Metapackage / empty base (no build system)",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The template whose menu label (or CLI identifier) is `label`.
|
/// The template whose menu label (or CLI identifier) is `label`.
|
||||||
pub fn from_label(label: &str) -> Option<TemplateId> {
|
pub fn from_label(label: &str) -> Option<TemplateId> {
|
||||||
TemplateId::all()
|
templates::from_label(label)
|
||||||
.into_iter()
|
|
||||||
.find(|id| id.display_name() == label || id.as_str() == label)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +189,10 @@ impl OrigOrigin {
|
|||||||
|
|
||||||
/// Upstream license of the package: a curated SPDX list plus a free-text
|
/// Upstream license of the package: a curated SPDX list plus a free-text
|
||||||
/// fallback for anything else (including "unknown" until the user picks one).
|
/// fallback for anything else (including "unknown" until the user picks one).
|
||||||
|
/// The curated identifiers, their accepted spellings and the menu labels
|
||||||
|
/// come from the bundled license table (`data/licenses.yml`); this enum
|
||||||
|
/// stays code because [`NewOptions`] and the template rendering match on
|
||||||
|
/// its variants — a consistency test keeps the two in lockstep.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum License {
|
pub enum License {
|
||||||
/// MIT
|
/// MIT
|
||||||
@@ -239,21 +219,34 @@ pub enum License {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl License {
|
impl License {
|
||||||
/// Map a license string to a [`License`]: curated SPDX identifiers are
|
/// Map a license string to a [`License`]: the curated SPDX identifiers
|
||||||
/// matched case-insensitively, anything else becomes
|
/// of the bundled license table (`data/licenses.yml`, matched through
|
||||||
/// [`License::Custom`] verbatim.
|
/// their spellings, case-insensitively) become their variant,
|
||||||
|
/// anything else becomes [`License::Custom`] verbatim (lowercased).
|
||||||
pub fn parse(s: &str) -> License {
|
pub fn parse(s: &str) -> License {
|
||||||
match s.to_ascii_lowercase().as_str() {
|
match licenses::entry_for_spelling(s) {
|
||||||
"mit" => License::Mit,
|
Some(entry) => Self::from_spdx(&entry.id)
|
||||||
"apache-2.0" => License::Apache2,
|
.expect("licenses.yml ids and License variants are kept in sync by a test"),
|
||||||
"gpl-2.0+" => License::Gpl2Plus,
|
None => License::Custom(s.to_ascii_lowercase()),
|
||||||
"gpl-3.0+" => License::Gpl3Plus,
|
}
|
||||||
"lgpl-2.1+" => License::Lgpl21Plus,
|
}
|
||||||
"lgpl-3.0+" => License::Lgpl3Plus,
|
|
||||||
"bsd-2-clause" => License::Bsd2Clause,
|
/// The variant of a curated license's SPDX identifier — the inverse of
|
||||||
"bsd-3-clause" => License::Bsd3Clause,
|
/// [`License::spdx`] for the non-`Custom` variants, `None` for any
|
||||||
"isc" => License::Isc,
|
/// other string. The ids accepted here are locked to the bundled
|
||||||
other => License::Custom(other.to_string()),
|
/// license table by a consistency test.
|
||||||
|
fn from_spdx(id: &str) -> Option<License> {
|
||||||
|
match id {
|
||||||
|
"MIT" => Some(License::Mit),
|
||||||
|
"Apache-2.0" => Some(License::Apache2),
|
||||||
|
"GPL-2.0+" => Some(License::Gpl2Plus),
|
||||||
|
"GPL-3.0+" => Some(License::Gpl3Plus),
|
||||||
|
"LGPL-2.1+" => Some(License::Lgpl21Plus),
|
||||||
|
"LGPL-3.0+" => Some(License::Lgpl3Plus),
|
||||||
|
"BSD-2-Clause" => Some(License::Bsd2Clause),
|
||||||
|
"BSD-3-Clause" => Some(License::Bsd3Clause),
|
||||||
|
"ISC" => Some(License::Isc),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,12 +267,11 @@ impl License {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// SPDX license data page URL (without the trailing `+` of the
|
/// SPDX license data page URL (without the trailing `+` of the
|
||||||
/// "or later" spellings), for the copyright reference paragraph.
|
/// "or later" spellings), for the copyright reference paragraph: the
|
||||||
|
/// identifier substituted into the URL template of the bundled
|
||||||
|
/// license table.
|
||||||
pub fn spdx_url(&self) -> String {
|
pub fn spdx_url(&self) -> String {
|
||||||
format!(
|
licenses::url_template().replace("{id}", self.spdx().trim_end_matches('+'))
|
||||||
"https://spdx.org/licenses/{}.html",
|
|
||||||
self.spdx().trim_end_matches('+')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,7 +625,7 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
|||||||
SourceDir::Skeleton => {
|
SourceDir::Skeleton => {
|
||||||
if cli.defaults {
|
if cli.defaults {
|
||||||
log::info!("No language given, --defaults picks the 'empty' template");
|
log::info!("No language given, --defaults picks the 'empty' template");
|
||||||
Some(TemplateId::Empty)
|
Some(TemplateId::EMPTY)
|
||||||
} else {
|
} else {
|
||||||
missing.push(format!(
|
missing.push(format!(
|
||||||
"--lang <{}|...> (no language given and there is nothing \
|
"--lang <{}|...> (no language given and there is nothing \
|
||||||
@@ -678,7 +670,7 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
|||||||
the 'empty' template",
|
the 'empty' template",
|
||||||
dir.display()
|
dir.display()
|
||||||
);
|
);
|
||||||
Some(TemplateId::Empty)
|
Some(TemplateId::EMPTY)
|
||||||
} else {
|
} else {
|
||||||
missing.push(
|
missing.push(
|
||||||
"--lang <id> (could not detect a build system; \
|
"--lang <id> (could not detect a build system; \
|
||||||
@@ -828,7 +820,7 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
|||||||
|
|
||||||
Ok(NewOptions {
|
Ok(NewOptions {
|
||||||
name,
|
name,
|
||||||
template: template.unwrap_or(TemplateId::Empty),
|
template: template.unwrap_or(TemplateId::EMPTY),
|
||||||
source_dir,
|
source_dir,
|
||||||
upstream_version,
|
upstream_version,
|
||||||
revision,
|
revision,
|
||||||
@@ -975,7 +967,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn template_ids_roundtrip() {
|
fn template_ids_roundtrip() {
|
||||||
for id in TemplateId::all() {
|
for id in TemplateId::all().iter().copied() {
|
||||||
assert_eq!(TemplateId::parse(id.as_str()).unwrap(), id);
|
assert_eq!(TemplateId::parse(id.as_str()).unwrap(), id);
|
||||||
// Every id resolves from its menu label too, and labels are
|
// Every id resolves from its menu label too, and labels are
|
||||||
// unique.
|
// unique.
|
||||||
@@ -984,9 +976,9 @@ mod tests {
|
|||||||
assert!(TemplateId::parse("cobol").is_err());
|
assert!(TemplateId::parse("cobol").is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TemplateId::from_label("Rust (Cargo.toml)"),
|
TemplateId::from_label("Rust (Cargo.toml)"),
|
||||||
Some(TemplateId::Rust)
|
Some(TemplateId::RUST)
|
||||||
);
|
);
|
||||||
assert_eq!(TemplateId::from_label("rust"), Some(TemplateId::Rust));
|
assert_eq!(TemplateId::from_label("rust"), Some(TemplateId::RUST));
|
||||||
assert_eq!(TemplateId::from_label("nope"), None);
|
assert_eq!(TemplateId::from_label("nope"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1082,7 +1074,7 @@ mod tests {
|
|||||||
// The full version round-trips through DebianVersion.
|
// The full version round-trips through DebianVersion.
|
||||||
let opts = NewOptions {
|
let opts = NewOptions {
|
||||||
name: "t".into(),
|
name: "t".into(),
|
||||||
template: TemplateId::Empty,
|
template: TemplateId::EMPTY,
|
||||||
source_dir: SourceDir::Here,
|
source_dir: SourceDir::Here,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -1177,6 +1169,56 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bundled license table and the `License` enum are two halves of
|
||||||
|
/// one list and must never drift apart: every entry id must map to a
|
||||||
|
/// variant (and the entry must accept its own id as a spelling, so
|
||||||
|
/// the menu answers parse back), every variant must have an entry,
|
||||||
|
/// and the counts must agree. The hardcoded variant list here is the
|
||||||
|
/// lock: adding a variant or a YAML entry without the other half
|
||||||
|
/// fails this test.
|
||||||
|
#[test]
|
||||||
|
fn licenses_table_and_enum_are_in_sync() {
|
||||||
|
let variants = [
|
||||||
|
License::Mit,
|
||||||
|
License::Apache2,
|
||||||
|
License::Gpl2Plus,
|
||||||
|
License::Gpl3Plus,
|
||||||
|
License::Lgpl21Plus,
|
||||||
|
License::Lgpl3Plus,
|
||||||
|
License::Bsd2Clause,
|
||||||
|
License::Bsd3Clause,
|
||||||
|
License::Isc,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
licenses::entries().len(),
|
||||||
|
variants.len(),
|
||||||
|
"licenses.yml and the License enum carry a different number of licenses"
|
||||||
|
);
|
||||||
|
for variant in variants {
|
||||||
|
let entry = licenses::entries()
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.id == variant.spdx())
|
||||||
|
.unwrap_or_else(|| panic!("no licenses.yml entry for '{}'", variant.spdx()));
|
||||||
|
assert_eq!(entry.id, variant.spdx());
|
||||||
|
assert!(
|
||||||
|
entry
|
||||||
|
.spellings
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.eq_ignore_ascii_case(&entry.id)),
|
||||||
|
"entry '{}' must accept its own id as a spelling",
|
||||||
|
entry.id
|
||||||
|
);
|
||||||
|
// The menu label parses back into the same variant.
|
||||||
|
assert_eq!(License::parse(&entry.menu), variant);
|
||||||
|
}
|
||||||
|
for entry in licenses::entries() {
|
||||||
|
let variant = License::from_spdx(&entry.id).unwrap_or_else(|| {
|
||||||
|
panic!("licenses.yml entry '{}' has no License variant", entry.id)
|
||||||
|
});
|
||||||
|
assert_eq!(variant.spdx(), entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collision_detection() {
|
fn collision_detection() {
|
||||||
assert!(check_file_collisions(&["a".into(), "b".into()]).is_ok());
|
assert!(check_file_collisions(&["a".into(), "b".into()]).is_ok());
|
||||||
@@ -1234,7 +1276,7 @@ mod tests {
|
|||||||
let opts = resolve(cli).await.unwrap();
|
let opts = resolve(cli).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(opts.name, "my-tool");
|
assert_eq!(opts.name, "my-tool");
|
||||||
assert_eq!(opts.template, TemplateId::Empty);
|
assert_eq!(opts.template, TemplateId::EMPTY);
|
||||||
assert!(matches!(opts.source_dir, SourceDir::Path(_)));
|
assert!(matches!(opts.source_dir, SourceDir::Path(_)));
|
||||||
assert_eq!(opts.upstream_version, "0.1.0");
|
assert_eq!(opts.upstream_version, "0.1.0");
|
||||||
assert_eq!(opts.revision, 1);
|
assert_eq!(opts.revision, 1);
|
||||||
@@ -1275,7 +1317,7 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let opts = resolve(cli).await.unwrap();
|
let opts = resolve(cli).await.unwrap();
|
||||||
assert_eq!(opts.template, TemplateId::Go);
|
assert_eq!(opts.template, TemplateId::GO);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1537,10 +1579,7 @@ mod tests {
|
|||||||
resolve(cli).await.unwrap().orig,
|
resolve(cli).await.unwrap().orig,
|
||||||
Some(OrigOrigin::Release {
|
Some(OrigOrigin::Release {
|
||||||
tag: "v1.2.3".to_string(),
|
tag: "v1.2.3".to_string(),
|
||||||
forge: crate::new::origin::Forge::GitHub {
|
forge: crate::new::origin::Forge::parse("https://github.com/foo/bar.git").unwrap(),
|
||||||
owner: "foo".into(),
|
|
||||||
repo: "bar".into()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -1535,10 +1535,8 @@ mod tests {
|
|||||||
"0.1.0",
|
"0.1.0",
|
||||||
&OrigOrigin::Release {
|
&OrigOrigin::Release {
|
||||||
tag: "v0.1.0".to_string(),
|
tag: "v0.1.0".to_string(),
|
||||||
forge: Forge::GitHub {
|
forge: Forge::parse("https://github.com/pkh-nonexistent-org/pkh-nonexistent-repo")
|
||||||
owner: "pkh-nonexistent-org".into(),
|
.unwrap(),
|
||||||
repo: "pkh-nonexistent-repo".into(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|||||||
+137
-53
@@ -8,48 +8,85 @@
|
|||||||
//! - is HEAD exactly on a tag, and which upstream version does it name,
|
//! - is HEAD exactly on a tag, and which upstream version does it name,
|
||||||
//! - which is the last tag reachable from HEAD (for the
|
//! - which is the last tag reachable from HEAD (for the
|
||||||
//! `<tag>+git<YYYYMMDD>.<hash>` version scheme),
|
//! `<tag>+git<YYYYMMDD>.<hash>` version scheme),
|
||||||
//! - where does the `origin` remote point: only `github.com` and
|
//! - where does the `origin` remote point: only the hosts of the bundled
|
||||||
//! `gitlab.com` are recognized as forges — self-hosted GitLab instances
|
//! forge table (`data/forges.yml`) are recognized as forges — self-hosted
|
||||||
//! are deliberately not (the release-download URL shapes differ).
|
//! GitLab instances are deliberately not part of it (the
|
||||||
|
//! release-download URL shapes differ), and the table also carries each
|
||||||
|
//! forge's tarball URL templates.
|
||||||
//!
|
//!
|
||||||
//! Detection never touches the network: adding a remote stores its URL in
|
//! Detection never touches the network: adding a remote stores its URL in
|
||||||
//! the local config only, which is all this module reads.
|
//! the local config only, which is all this module reads.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
/// A forge hosting the project, parsed from the `origin` remote URL.
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::data::embed_data;
|
||||||
|
|
||||||
|
/// Family a forge belongs to. Documentation of which URL-shape family a
|
||||||
|
/// table entry belongs to — the tarball templates fully describe the
|
||||||
|
/// URLs, so nothing branches on the kind (yet). Parsed strictly: an
|
||||||
|
/// unknown kind fails the load rather than being silently ignored.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum ForgeKind {
|
||||||
|
/// `github.com` and its codeload archive service
|
||||||
|
GitHub,
|
||||||
|
/// `gitlab.com`
|
||||||
|
GitLab,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry of the bundled forge table: the release-tarball URL
|
||||||
|
/// templates of a forge host
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ForgeEntry {
|
||||||
|
/// Family of the forge (see [`ForgeKind`]). Kept even though nothing
|
||||||
|
/// branches on it (the templates fully describe the URLs): it
|
||||||
|
/// documents the entry's URL-shape family and the strict enum
|
||||||
|
/// validates it at load time.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
kind: ForgeKind,
|
||||||
|
/// Release-tarball URL templates, best candidate first (tried
|
||||||
|
/// sequentially by the download)
|
||||||
|
tarball_templates: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bundled forge table (`data/forges.yml`): host name → entry
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ForgesData {
|
||||||
|
/// Recognized forge hosts
|
||||||
|
forges: HashMap<String, ForgeEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_data! {
|
||||||
|
static ref FORGES_DATA: ForgesData = "../../data/forges.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A forge hosting the project, parsed from the `origin` remote URL: one
|
||||||
|
/// of the hosts of the bundled forge table, plus the repository it points
|
||||||
|
/// at.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Forge {
|
pub struct Forge {
|
||||||
/// `github.com/<owner>/<repo>`
|
/// Host name of the forge (the key of its `forges.yml` entry).
|
||||||
GitHub {
|
host: &'static str,
|
||||||
/// Repository owner (user or organization).
|
/// Repository owner (user or organization).
|
||||||
owner: String,
|
owner: String,
|
||||||
/// Repository name, without the `.git` suffix.
|
/// Repository name, without the `.git` suffix.
|
||||||
repo: String,
|
repo: String,
|
||||||
},
|
|
||||||
/// `gitlab.com/<owner>/<repo>`
|
|
||||||
GitLab {
|
|
||||||
/// Repository owner (user or group).
|
|
||||||
owner: String,
|
|
||||||
/// Repository name, without the `.git` suffix.
|
|
||||||
repo: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Forge {
|
impl Forge {
|
||||||
/// Host name of the forge.
|
/// Host name of the forge.
|
||||||
pub fn host(&self) -> &'static str {
|
pub fn host(&self) -> &'static str {
|
||||||
match self {
|
self.host
|
||||||
Forge::GitHub { .. } => "github.com",
|
|
||||||
Forge::GitLab { .. } => "gitlab.com",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a remote URL into a [`Forge`], accepting the `https://`,
|
/// Parse a remote URL into a [`Forge`], accepting the `https://`,
|
||||||
/// `http://`, `git://` and `git@host:` spellings. Only `github.com` and
|
/// `http://`, `git://` and `git@host:` spellings. Only the hosts
|
||||||
/// `gitlab.com` are recognized; anything else (self-hosted GitLab,
|
/// listed in the bundled forge table are recognized; anything else
|
||||||
/// Bitbucket, plain URLs…) yields `None`.
|
/// (self-hosted GitLab, Bitbucket, plain URLs…) yields `None`.
|
||||||
pub fn parse(url: &str) -> Option<Forge> {
|
pub fn parse(url: &str) -> Option<Forge> {
|
||||||
let url = url.trim();
|
let url = url.trim();
|
||||||
// Normalize `git@host:path` to `host/path` and strip any scheme.
|
// Normalize `git@host:path` to `host/path` and strip any scheme.
|
||||||
@@ -74,34 +111,34 @@ impl Forge {
|
|||||||
if owner.is_empty() || repo.is_empty() {
|
if owner.is_empty() || repo.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match host {
|
// The table key doubles as the Forge's host, so the two can never
|
||||||
"github.com" => Some(Forge::GitHub {
|
// disagree about how the forge is spelled.
|
||||||
|
let (host, _entry) = FORGES_DATA.forges.get_key_value(host)?;
|
||||||
|
Some(Forge {
|
||||||
|
host,
|
||||||
owner: owner.to_string(),
|
owner: owner.to_string(),
|
||||||
repo: repo.to_string(),
|
repo: repo.to_string(),
|
||||||
}),
|
})
|
||||||
"gitlab.com" => Some(Forge::GitLab {
|
|
||||||
owner: owner.to_string(),
|
|
||||||
repo: repo.to_string(),
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Release-tarball URLs of `tag`, best candidate first. GitHub prefers
|
/// Release-tarball URLs of `tag`, best candidate first: the templates
|
||||||
/// the codeload direct link (no redirect) and falls back to the
|
/// of the forge's table entry, with `{owner}`, `{repo}` and `{tag}`
|
||||||
/// `github.com` archive URL; GitLab has a single archive URL.
|
/// substituted, in file order (the download tries them sequentially).
|
||||||
pub fn release_tarball_urls(&self, tag: &str) -> Vec<String> {
|
pub fn release_tarball_urls(&self, tag: &str) -> Vec<String> {
|
||||||
match self {
|
let entry = FORGES_DATA
|
||||||
Forge::GitHub { owner, repo } => vec![
|
.forges
|
||||||
format!("https://codeload.github.com/{owner}/{repo}/tar.gz/refs/tags/{tag}"),
|
.get(self.host)
|
||||||
format!("https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.tar.gz"),
|
.expect("the host of a parsed Forge is a key of the forge table");
|
||||||
],
|
entry
|
||||||
Forge::GitLab { owner, repo } => {
|
.tarball_templates
|
||||||
vec![format!(
|
.iter()
|
||||||
"https://gitlab.com/{owner}/{repo}/-/archive/{tag}/{repo}-{tag}.tar.gz"
|
.map(|template| {
|
||||||
)]
|
template
|
||||||
}
|
.replace("{owner}", &self.owner)
|
||||||
}
|
.replace("{repo}", &self.repo)
|
||||||
|
.replace("{tag}", tag)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,35 +336,40 @@ mod tests {
|
|||||||
fn forge_parses_remote_url_shapes() {
|
fn forge_parses_remote_url_shapes() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("https://github.com/foo/bar.git"),
|
Forge::parse("https://github.com/foo/bar.git"),
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("git@github.com:foo/bar.git"),
|
Forge::parse("git@github.com:foo/bar.git"),
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("git://github.com/foo/bar"),
|
Forge::parse("git://github.com/foo/bar"),
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("https://gitlab.com/foo/bar/-/tree/main"),
|
Forge::parse("https://gitlab.com/foo/bar/-/tree/main"),
|
||||||
Some(Forge::GitLab {
|
Some(Forge {
|
||||||
|
host: "gitlab.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("ssh://git@gitlab.com/foo/bar.git"),
|
Forge::parse("ssh://git@gitlab.com/foo/bar.git"),
|
||||||
Some(Forge::GitLab {
|
Some(Forge {
|
||||||
|
host: "gitlab.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
@@ -342,7 +384,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn forge_release_tarball_urls() {
|
fn forge_release_tarball_urls() {
|
||||||
let gh = Forge::GitHub {
|
let gh = Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into(),
|
repo: "bar".into(),
|
||||||
};
|
};
|
||||||
@@ -353,7 +396,8 @@ mod tests {
|
|||||||
"https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz".to_string(),
|
"https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz".to_string(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
let gl = Forge::GitLab {
|
let gl = Forge {
|
||||||
|
host: "gitlab.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into(),
|
repo: "bar".into(),
|
||||||
};
|
};
|
||||||
@@ -363,6 +407,45 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every table entry must be substitutable: a template missing one of
|
||||||
|
/// the placeholders would download from a literal `{name}` URL.
|
||||||
|
#[test]
|
||||||
|
fn forge_templates_carry_every_placeholder() {
|
||||||
|
for (host, entry) in &FORGES_DATA.forges {
|
||||||
|
assert!(
|
||||||
|
!entry.tarball_templates.is_empty(),
|
||||||
|
"forge '{host}' has no tarball template"
|
||||||
|
);
|
||||||
|
for template in &entry.tarball_templates {
|
||||||
|
for placeholder in ["{owner}", "{repo}", "{tag}"] {
|
||||||
|
assert!(
|
||||||
|
template.contains(placeholder),
|
||||||
|
"template '{template}' of forge '{host}' lacks {placeholder}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `kind` field is validated at load time: an unknown forge family
|
||||||
|
/// fails the parse instead of being silently accepted.
|
||||||
|
#[test]
|
||||||
|
fn forge_kind_is_validated() {
|
||||||
|
assert!(serde_yaml::from_str::<ForgeEntry>("kind: github").is_err());
|
||||||
|
assert!(
|
||||||
|
serde_yaml::from_str::<ForgeEntry>(
|
||||||
|
"kind: github\ntarball_templates: ['https://h/{owner}/{repo}/{tag}']"
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
serde_yaml::from_str::<ForgeEntry>(
|
||||||
|
"kind: codeberg\ntarball_templates: ['https://h/{owner}/{repo}/{tag}']"
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sanitized_tag_versions() {
|
fn sanitized_tag_versions() {
|
||||||
assert_eq!(sanitized_tag_version("v1.2.3"), Some("1.2.3".to_string()));
|
assert_eq!(sanitized_tag_version("v1.2.3"), Some("1.2.3".to_string()));
|
||||||
@@ -466,7 +549,8 @@ mod tests {
|
|||||||
let origin = GitOrigin::detect(dir.path()).expect("detected");
|
let origin = GitOrigin::detect(dir.path()).expect("detected");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
origin.forge,
|
origin.forge,
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
|
|||||||
+63
-69
@@ -24,6 +24,7 @@ use indicatif::MultiProgress;
|
|||||||
|
|
||||||
use crate::new::detect::{self, Detection};
|
use crate::new::detect::{self, Detection};
|
||||||
use crate::new::git;
|
use crate::new::git;
|
||||||
|
use crate::new::licenses;
|
||||||
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
||||||
use crate::new::origin::GitOrigin;
|
use crate::new::origin::GitOrigin;
|
||||||
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
||||||
@@ -39,20 +40,6 @@ const SOURCE_PATH: &str = "Package the sources in another directory…";
|
|||||||
/// The "everything else" entry of the license menu.
|
/// The "everything else" entry of the license menu.
|
||||||
const LICENSE_OTHER: &str = "Other (enter a SPDX identifier)";
|
const LICENSE_OTHER: &str = "Other (enter a SPDX identifier)";
|
||||||
|
|
||||||
/// The curated SPDX identifiers of the license menu (without the free-text
|
|
||||||
/// entry), matching [`options::License::parse`]'s known spellings.
|
|
||||||
pub const KNOWN_LICENSES: [&str; 9] = [
|
|
||||||
"MIT",
|
|
||||||
"Apache-2.0",
|
|
||||||
"GPL-2.0+",
|
|
||||||
"GPL-3.0+",
|
|
||||||
"LGPL-2.1+",
|
|
||||||
"LGPL-3.0+",
|
|
||||||
"BSD-2-Clause",
|
|
||||||
"BSD-3-Clause",
|
|
||||||
"ISC",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Labels of the interactive `select` questions. `prompt::select` renders
|
/// Labels of the interactive `select` questions. `prompt::select` renders
|
||||||
/// `> <label><answer>` verbatim — unlike [`prompt::text`], it appends no
|
/// `> <label><answer>` verbatim — unlike [`prompt::text`], it appends no
|
||||||
/// formatting of its own — so each label carries its own separator:
|
/// formatting of its own — so each label carries its own separator:
|
||||||
@@ -152,7 +139,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
LanguageChoice::Ask(preselected) => {
|
LanguageChoice::Ask(preselected) => {
|
||||||
let menu = language_menu(&[]);
|
let menu = language_menu(&[]);
|
||||||
let default = preselected
|
let default = preselected
|
||||||
.unwrap_or(TemplateId::Empty)
|
.unwrap_or(TemplateId::EMPTY)
|
||||||
.display_name()
|
.display_name()
|
||||||
.to_string();
|
.to_string();
|
||||||
let id = select_template(&menu, &default)?;
|
let id = select_template(&menu, &default)?;
|
||||||
@@ -179,7 +166,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
// The rust toolchain pin of the packaged project does not travel into
|
// The rust toolchain pin of the packaged project does not travel into
|
||||||
// the chroot build: surface it now so a too-old pin is not a surprise
|
// the chroot build: surface it now so a too-old pin is not a surprise
|
||||||
// when `pkh deb` compiles with the distribution's rustc.
|
// when `pkh deb` compiles with the distribution's rustc.
|
||||||
let toolchain_pin = if template == TemplateId::Rust {
|
let toolchain_pin = if template == TemplateId::RUST {
|
||||||
probe.as_ref().and_then(|p| p.toolchain_pin.clone())
|
probe.as_ref().and_then(|p| p.toolchain_pin.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -403,7 +390,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
// same `validate_command` bar as `resolve` applies (which also
|
// same `validate_command` bar as `resolve` applies (which also
|
||||||
// requires a non-empty answer), so an unusable probe is withheld and
|
// requires a non-empty answer), so an unusable probe is withheld and
|
||||||
// invalid input re-asks here instead of failing late in `resolve`.
|
// invalid input re-asks here instead of failing late in `resolve`.
|
||||||
if cli.command.is_none() && template != TemplateId::Empty {
|
if cli.command.is_none() && template != TemplateId::EMPTY {
|
||||||
let default = probe
|
let default = probe
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.command.clone())
|
.and_then(|p| p.command.clone())
|
||||||
@@ -437,17 +424,26 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
cli.maintainer = Some(ask_text("Maintainer", &default, validate)?);
|
cli.maintainer = Some(ask_text("Maintainer", &default, validate)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. Target distribution.
|
// 11. Target distribution. The menu derives from the distro data pkh
|
||||||
|
// ships (sorted); ubuntu is moved to the front when present so it
|
||||||
|
// stays the menu's first entry and fallback default as it has always
|
||||||
|
// been — prompt::select positions on a default value, not an index.
|
||||||
if cli.dist.is_none() {
|
if cli.dist.is_none() {
|
||||||
let vendor = crate::build::env::current_vendor().to_lowercase();
|
let vendor = crate::build::env::current_vendor().to_lowercase();
|
||||||
let options = vec!["ubuntu".to_string(), "debian".to_string()];
|
let mut options = crate::distro_info::supported_dists();
|
||||||
|
if let Some(pos) = options.iter().position(|d| d == "ubuntu")
|
||||||
|
&& pos > 0
|
||||||
|
{
|
||||||
|
let ubuntu = options.remove(pos);
|
||||||
|
options.insert(0, ubuntu);
|
||||||
|
}
|
||||||
let default = if options.contains(&vendor) {
|
let default = if options.contains(&vendor) {
|
||||||
vendor
|
vendor
|
||||||
} else {
|
} else {
|
||||||
"ubuntu".to_string()
|
"ubuntu".to_string()
|
||||||
};
|
};
|
||||||
let answer = select_from(DIST_LABEL, &options, &default, |answer| {
|
let answer = select_from(DIST_LABEL, &options, &default, |answer| {
|
||||||
answer == "ubuntu" || answer == "debian"
|
options.contains(&answer.to_string())
|
||||||
})?;
|
})?;
|
||||||
cli.dist = Some(answer);
|
cli.dist = Some(answer);
|
||||||
}
|
}
|
||||||
@@ -473,7 +469,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 13. Metapackage Depends (empty template only).
|
// 13. Metapackage Depends (empty template only).
|
||||||
if template == TemplateId::Empty && cli.depends.is_empty() {
|
if template == TemplateId::EMPTY && cli.depends.is_empty() {
|
||||||
let validate = |answer: &str| options::validate_depends(answer).map(|_| ());
|
let validate = |answer: &str| options::validate_depends(answer).map(|_| ());
|
||||||
let answer = ask_text(
|
let answer = ask_text(
|
||||||
"Depends (metapackage, comma-separated, blank for an empty base)",
|
"Depends (metapackage, comma-separated, blank for an empty base)",
|
||||||
@@ -508,7 +504,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
// The meson/cmake opt-in question of the spec's template table: does the
|
// The meson/cmake opt-in question of the spec's template table: does the
|
||||||
// build resolve libraries through pkg-config? The project files prefill
|
// build resolve libraries through pkg-config? The project files prefill
|
||||||
// the default (dependency() / pkg_check_modules calls found).
|
// the default (dependency() / pkg_check_modules calls found).
|
||||||
if matches!(template, TemplateId::Meson | TemplateId::Cmake)
|
if matches!(template, TemplateId::MESON | TemplateId::CMAKE)
|
||||||
&& prompt::confirm(
|
&& prompt::confirm(
|
||||||
"Does the build resolve libraries through pkg-config (add it to Build-Depends)?",
|
"Does the build resolve libraries through pkg-config (add it to Build-Depends)?",
|
||||||
pkg_config_hint(&detect_dir, template),
|
pkg_config_hint(&detect_dir, template),
|
||||||
@@ -518,7 +514,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wizard-only extras (default off).
|
// Wizard-only extras (default off).
|
||||||
if template != TemplateId::Empty
|
if template != TemplateId::EMPTY
|
||||||
&& prompt::confirm(
|
&& prompt::confirm(
|
||||||
"Add an autopkgtest smoke test (debian/tests/control)?",
|
"Add an autopkgtest smoke test (debian/tests/control)?",
|
||||||
false,
|
false,
|
||||||
@@ -690,8 +686,8 @@ fn same_directory(a: &std::path::Path, b: &std::path::Path) -> bool {
|
|||||||
/// CMakeLists.txt).
|
/// CMakeLists.txt).
|
||||||
fn pkg_config_hint(dir: &std::path::Path, template: TemplateId) -> bool {
|
fn pkg_config_hint(dir: &std::path::Path, template: TemplateId) -> bool {
|
||||||
let (file, needles): (&str, &[&str]) = match template {
|
let (file, needles): (&str, &[&str]) = match template {
|
||||||
TemplateId::Meson => ("meson.build", &["dependency("]),
|
TemplateId::MESON => ("meson.build", &["dependency("]),
|
||||||
TemplateId::Cmake => (
|
TemplateId::CMAKE => (
|
||||||
"CMakeLists.txt",
|
"CMakeLists.txt",
|
||||||
&[
|
&[
|
||||||
"pkg_check_modules",
|
"pkg_check_modules",
|
||||||
@@ -731,7 +727,8 @@ fn language_menu(candidates: &[TemplateId]) -> Vec<String> {
|
|||||||
.copied()
|
.copied()
|
||||||
.chain(
|
.chain(
|
||||||
TemplateId::all()
|
TemplateId::all()
|
||||||
.into_iter()
|
.iter()
|
||||||
|
.copied()
|
||||||
.filter(|id| !candidates.contains(id)),
|
.filter(|id| !candidates.contains(id)),
|
||||||
)
|
)
|
||||||
.map(|id| id.display_name().to_string())
|
.map(|id| id.display_name().to_string())
|
||||||
@@ -774,29 +771,32 @@ fn language_choice(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The license menu: the curated SPDX list plus the free-text entry.
|
/// The license menu: the `menu` labels of the bundled license table
|
||||||
|
/// (`data/licenses.yml`) plus the free-text entry.
|
||||||
fn license_menu() -> Vec<String> {
|
fn license_menu() -> Vec<String> {
|
||||||
KNOWN_LICENSES
|
licenses::entries()
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.map(|entry| entry.menu.clone())
|
||||||
.map(str::to_string)
|
|
||||||
.chain(std::iter::once(LICENSE_OTHER.to_string()))
|
.chain(std::iter::once(LICENSE_OTHER.to_string()))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The defaults of the license question for a probed SPDX identifier: the
|
/// The defaults of the license question for a probed SPDX identifier: the
|
||||||
/// matching curated entry (case-insensitive) is preselected; anything else
|
/// matching curated entry (case-insensitive over the menu labels) is
|
||||||
/// preselects the free-text entry prefilled with the probe. Without a probe
|
/// preselected; anything else preselects the free-text entry prefilled with
|
||||||
/// the curated list defaults to MIT.
|
/// the probe. Without a probe the curated list defaults to MIT (the first
|
||||||
|
/// menu entry of the bundled table).
|
||||||
fn license_question_default(probe_license: Option<&str>) -> (String, String) {
|
fn license_question_default(probe_license: Option<&str>) -> (String, String) {
|
||||||
match probe_license {
|
match probe_license {
|
||||||
Some(license) => match KNOWN_LICENSES
|
Some(license) => {
|
||||||
|
match licenses::entries()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|k| k.eq_ignore_ascii_case(license))
|
.find(|entry| entry.menu.eq_ignore_ascii_case(license))
|
||||||
{
|
{
|
||||||
Some(known) => ((*known).to_string(), String::new()),
|
Some(known) => (known.menu.clone(), String::new()),
|
||||||
None => (LICENSE_OTHER.to_string(), license.to_string()),
|
None => (LICENSE_OTHER.to_string(), license.to_string()),
|
||||||
},
|
}
|
||||||
|
}
|
||||||
None => ("MIT".to_string(), String::new()),
|
None => ("MIT".to_string(), String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1034,12 +1034,12 @@ pub fn summary_text(opts: &NewOptions, toolchain_pin: Option<&str>) -> String {
|
|||||||
" debian/control Source + 1 binary (Architecture: {})",
|
" debian/control Source + 1 binary (Architecture: {})",
|
||||||
template.architecture(opts)
|
template.architecture(opts)
|
||||||
));
|
));
|
||||||
if opts.template == TemplateId::Empty {
|
if opts.template == TemplateId::EMPTY {
|
||||||
// The Depends list is the payload of the metapackage flavor.
|
// The Depends list is the payload of the metapackage flavor.
|
||||||
if !opts.depends.is_empty() {
|
if !opts.depends.is_empty() {
|
||||||
lines.push(format!(" Depends {}", opts.depends.join(", ")));
|
lines.push(format!(" Depends {}", opts.depends.join(", ")));
|
||||||
}
|
}
|
||||||
} else if opts.template == TemplateId::Rust {
|
} else if opts.template == TemplateId::RUST {
|
||||||
// Nothing is vendored yet at this point: only announce that the
|
// Nothing is vendored yet at this point: only announce that the
|
||||||
// generation will attempt it.
|
// generation will attempt it.
|
||||||
lines.push(
|
lines.push(
|
||||||
@@ -1095,7 +1095,7 @@ pub fn summary_text(opts: &NewOptions, toolchain_pin: Option<&str>) -> String {
|
|||||||
lines.push(format!(" + {} (new skeleton)", names.join(", ")));
|
lines.push(format!(" + {} (new skeleton)", names.join(", ")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opts.template == TemplateId::Rust && templates::find_on_path("cargo").is_none() {
|
if opts.template == TemplateId::RUST && templates::find_on_path("cargo").is_none() {
|
||||||
lines.push(
|
lines.push(
|
||||||
" ! cargo not found on PATH: dependencies cannot be vendored at \
|
" ! cargo not found on PATH: dependencies cannot be vendored at \
|
||||||
scaffold time; the package will not build until you run \
|
scaffold time; the package will not build until you run \
|
||||||
@@ -1163,7 +1163,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn language_menu_lists_candidates_first() {
|
fn language_menu_lists_candidates_first() {
|
||||||
let menu = language_menu(&[Tid::Makefile, Tid::Rust]);
|
let menu = language_menu(&[Tid::MAKEFILE, Tid::RUST]);
|
||||||
assert_eq!(menu[0], "Generic (Makefile)");
|
assert_eq!(menu[0], "Generic (Makefile)");
|
||||||
assert_eq!(menu[1], "Rust (Cargo.toml)");
|
assert_eq!(menu[1], "Rust (Cargo.toml)");
|
||||||
// The remaining seven follow in registry order, no duplicates.
|
// The remaining seven follow in registry order, no duplicates.
|
||||||
@@ -1180,8 +1180,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn language_choice_flag_wins_over_detection() {
|
fn language_choice_flag_wins_over_detection() {
|
||||||
let detections = [
|
let detections = [
|
||||||
Detection::Single(Tid::Rust),
|
Detection::Single(Tid::RUST),
|
||||||
Detection::Ambiguous(vec![Tid::Rust, Tid::Python]),
|
Detection::Ambiguous(vec![Tid::RUST, Tid::PYTHON]),
|
||||||
Detection::Empty,
|
Detection::Empty,
|
||||||
];
|
];
|
||||||
for detection in &detections {
|
for detection in &detections {
|
||||||
@@ -1202,21 +1202,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn language_choice_without_flag_follows_detection() {
|
fn language_choice_without_flag_follows_detection() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
language_choice(None, &Detection::Single(Tid::Rust), true),
|
language_choice(None, &Detection::Single(Tid::RUST), true),
|
||||||
LanguageChoice::Detected(Tid::Rust)
|
LanguageChoice::Detected(Tid::RUST)
|
||||||
);
|
);
|
||||||
// Skeleton run: ask, preselecting the detected ecosystem.
|
// Skeleton run: ask, preselecting the detected ecosystem.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
language_choice(None, &Detection::Single(Tid::Rust), false),
|
language_choice(None, &Detection::Single(Tid::RUST), false),
|
||||||
LanguageChoice::Ask(Some(Tid::Rust))
|
LanguageChoice::Ask(Some(Tid::RUST))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
language_choice(
|
language_choice(
|
||||||
None,
|
None,
|
||||||
&Detection::Ambiguous(vec![Tid::Go, Tid::Python]),
|
&Detection::Ambiguous(vec![Tid::GO, Tid::PYTHON]),
|
||||||
true
|
true
|
||||||
),
|
),
|
||||||
LanguageChoice::Ambiguous(vec![Tid::Go, Tid::Python])
|
LanguageChoice::Ambiguous(vec![Tid::GO, Tid::PYTHON])
|
||||||
);
|
);
|
||||||
// Nothing detected: plain menu, the empty template preselected.
|
// Nothing detected: plain menu, the empty template preselected.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1228,7 +1228,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn license_menu_and_defaults() {
|
fn license_menu_and_defaults() {
|
||||||
let menu = license_menu();
|
let menu = license_menu();
|
||||||
assert_eq!(menu.len(), KNOWN_LICENSES.len() + 1);
|
assert_eq!(menu.len(), licenses::entries().len() + 1);
|
||||||
assert_eq!(menu[0], "MIT");
|
assert_eq!(menu[0], "MIT");
|
||||||
assert_eq!(menu.last().unwrap(), LICENSE_OTHER);
|
assert_eq!(menu.last().unwrap(), LICENSE_OTHER);
|
||||||
|
|
||||||
@@ -1286,10 +1286,7 @@ mod tests {
|
|||||||
fn orig_origin_choices_preorder_by_detection() {
|
fn orig_origin_choices_preorder_by_detection() {
|
||||||
use crate::new::origin::Forge;
|
use crate::new::origin::Forge;
|
||||||
let tagged_forge = GitOrigin {
|
let tagged_forge = GitOrigin {
|
||||||
forge: Some(Forge::GitHub {
|
forge: Some(Forge::parse("https://github.com/foo/bar").unwrap()),
|
||||||
owner: "foo".into(),
|
|
||||||
repo: "bar".into(),
|
|
||||||
}),
|
|
||||||
head_tag: Some("v1.4.0".into()),
|
head_tag: Some("v1.4.0".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1331,12 +1328,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn summary_screen_shows_format_and_orig_origin() {
|
fn summary_screen_shows_format_and_orig_origin() {
|
||||||
// Native skeleton: the format row, no orig row.
|
// Native skeleton: the format row, no orig row.
|
||||||
let text = summary_text(&opts(Tid::Shell), None);
|
let text = summary_text(&opts(Tid::SHELL), None);
|
||||||
assert!(text.contains("debian/source/format 3.0 (native)"), "{text}");
|
assert!(text.contains("debian/source/format 3.0 (native)"), "{text}");
|
||||||
assert!(!text.contains("orig tarball"), "{text}");
|
assert!(!text.contains("orig tarball"), "{text}");
|
||||||
|
|
||||||
// Quilt over an existing project: both rows.
|
// Quilt over an existing project: both rows.
|
||||||
let mut quilt = opts(Tid::Shell);
|
let mut quilt = opts(Tid::SHELL);
|
||||||
quilt.source_dir = options::SourceDir::Here;
|
quilt.source_dir = options::SourceDir::Here;
|
||||||
quilt.source_format = options::SourceFormat::Quilt;
|
quilt.source_format = options::SourceFormat::Quilt;
|
||||||
quilt.orig = Some(options::OrigOrigin::GitArchive {
|
quilt.orig = Some(options::OrigOrigin::GitArchive {
|
||||||
@@ -1353,10 +1350,7 @@ mod tests {
|
|||||||
let mut release = quilt.clone();
|
let mut release = quilt.clone();
|
||||||
release.orig = Some(options::OrigOrigin::Release {
|
release.orig = Some(options::OrigOrigin::Release {
|
||||||
tag: "v0.14.0".to_string(),
|
tag: "v0.14.0".to_string(),
|
||||||
forge: crate::new::origin::Forge::GitLab {
|
forge: crate::new::origin::Forge::parse("https://gitlab.com/foo/bar").unwrap(),
|
||||||
owner: "foo".into(),
|
|
||||||
repo: "bar".into(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
let text = summary_text(&release, None);
|
let text = summary_text(&release, None);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1367,7 +1361,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn summary_screen_skeleton() {
|
fn summary_screen_skeleton() {
|
||||||
let text = summary_text(&opts(Tid::Makefile), None);
|
let text = summary_text(&opts(Tid::MAKEFILE), None);
|
||||||
assert!(
|
assert!(
|
||||||
text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"),
|
text.contains("mytool 0.1.0-1 · builds for ubuntu/resolute"),
|
||||||
"{text}"
|
"{text}"
|
||||||
@@ -1397,7 +1391,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn summary_screen_metapackage_shows_depends() {
|
fn summary_screen_metapackage_shows_depends() {
|
||||||
let mut o = opts(Tid::Empty);
|
let mut o = opts(Tid::EMPTY);
|
||||||
o.depends = vec!["hello".to_string(), "hello-data (>= 1.0)".to_string()];
|
o.depends = vec!["hello".to_string(), "hello-data (>= 1.0)".to_string()];
|
||||||
o.source_dir = options::SourceDir::Here;
|
o.source_dir = options::SourceDir::Here;
|
||||||
let text = summary_text(&o, None);
|
let text = summary_text(&o, None);
|
||||||
@@ -1413,7 +1407,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn summary_screen_release_and_extras() {
|
fn summary_screen_release_and_extras() {
|
||||||
let mut o = opts(Tid::Shell);
|
let mut o = opts(Tid::SHELL);
|
||||||
o.release = true;
|
o.release = true;
|
||||||
o.autopkgtest = true;
|
o.autopkgtest = true;
|
||||||
o.watch = Some("version=4\n".to_string());
|
o.watch = Some("version=4\n".to_string());
|
||||||
@@ -1431,7 +1425,7 @@ mod tests {
|
|||||||
/// yet (regression: it claimed "(vendored)" before generating).
|
/// yet (regression: it claimed "(vendored)" before generating).
|
||||||
#[test]
|
#[test]
|
||||||
fn summary_screen_rust_does_not_presume_vendoring() {
|
fn summary_screen_rust_does_not_presume_vendoring() {
|
||||||
let text = summary_text(&opts(Tid::Rust), None);
|
let text = summary_text(&opts(Tid::RUST), None);
|
||||||
assert!(
|
assert!(
|
||||||
text.contains("cargo build --release --offline (vendored at generation)"),
|
text.contains("cargo build --release --offline (vendored at generation)"),
|
||||||
"{text}"
|
"{text}"
|
||||||
@@ -1443,17 +1437,17 @@ mod tests {
|
|||||||
/// (rust template only), flagged as ignored by the chroot build.
|
/// (rust template only), flagged as ignored by the chroot build.
|
||||||
#[test]
|
#[test]
|
||||||
fn summary_screen_shows_the_toolchain_pin() {
|
fn summary_screen_shows_the_toolchain_pin() {
|
||||||
let text = summary_text(&opts(Tid::Rust), Some("1.98.0"));
|
let text = summary_text(&opts(Tid::RUST), Some("1.98.0"));
|
||||||
assert!(
|
assert!(
|
||||||
text.contains("rust-toolchain 1.98.0 (ignored by the chroot build)"),
|
text.contains("rust-toolchain 1.98.0 (ignored by the chroot build)"),
|
||||||
"{text}"
|
"{text}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// No pin, no row.
|
// No pin, no row.
|
||||||
assert!(!summary_text(&opts(Tid::Rust), None).contains("rust-toolchain"));
|
assert!(!summary_text(&opts(Tid::RUST), None).contains("rust-toolchain"));
|
||||||
// A pin under a template other than rust is not shown either (the
|
// A pin under a template other than rust is not shown either (the
|
||||||
// pin only matters for a cargo build).
|
// pin only matters for a cargo build).
|
||||||
assert!(!summary_text(&opts(Tid::Go), Some("1.98.0")).contains("rust-toolchain"));
|
assert!(!summary_text(&opts(Tid::GO), Some("1.98.0")).contains("rust-toolchain"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The git-init question is only asked when a git init would actually
|
/// The git-init question is only asked when a git init would actually
|
||||||
@@ -1602,7 +1596,7 @@ mod tests {
|
|||||||
|
|
||||||
// The initial pass over dir A.
|
// The initial pass over dir A.
|
||||||
let (detection_a, probe_a) = detect_and_probe(dir_a.path());
|
let (detection_a, probe_a) = detect_and_probe(dir_a.path());
|
||||||
assert_eq!(detection_a, Detection::Single(Tid::Rust));
|
assert_eq!(detection_a, Detection::Single(Tid::RUST));
|
||||||
let probe_a = probe_a.expect("dir A is a rust project");
|
let probe_a = probe_a.expect("dir A is a rust project");
|
||||||
assert_eq!(probe_a.name.as_deref(), Some("alpha"));
|
assert_eq!(probe_a.name.as_deref(), Some("alpha"));
|
||||||
assert_eq!(probe_a.version.as_deref(), Some("0.1.0"));
|
assert_eq!(probe_a.version.as_deref(), Some("0.1.0"));
|
||||||
@@ -1611,7 +1605,7 @@ mod tests {
|
|||||||
// The user chose dir B instead: the refreshed probe comes from B,
|
// The user chose dir B instead: the refreshed probe comes from B,
|
||||||
// never from A.
|
// never from A.
|
||||||
let (detection_b, probe_b) = detect_and_probe(dir_b.path());
|
let (detection_b, probe_b) = detect_and_probe(dir_b.path());
|
||||||
assert_eq!(detection_b, Detection::Single(Tid::Rust));
|
assert_eq!(detection_b, Detection::Single(Tid::RUST));
|
||||||
let probe_b = probe_b.expect("dir B is a rust project");
|
let probe_b = probe_b.expect("dir B is a rust project");
|
||||||
assert_eq!(probe_b.name.as_deref(), Some("beta"));
|
assert_eq!(probe_b.name.as_deref(), Some("beta"));
|
||||||
assert_eq!(probe_b.version.as_deref(), Some("2.9.9"));
|
assert_eq!(probe_b.version.as_deref(), Some("2.9.9"));
|
||||||
|
|||||||
@@ -1,71 +1,34 @@
|
|||||||
//! The `autotools` template: a C project with a `configure.ac` built through
|
//! The `autotools` template: a C project with a `configure.ac` built through
|
||||||
//! debhelper's auto-detection (dh runs `autoreconf` itself when it finds
|
//! debhelper's auto-detection (dh runs `autoreconf` itself when it finds
|
||||||
//! `configure.ac`, debhelper ≥ 10 — no override needed).
|
//! `configure.ac`, debhelper ≥ 10 — no override needed).
|
||||||
|
//!
|
||||||
|
//! The metadata and the `configure.ac`/`Makefile.am`/`hello.c` skeleton
|
||||||
|
//! bodies are manifest data (`data/templates/autotools/manifest.yml`); the
|
||||||
|
//! logic half here is the `AC_INIT` probe and the GNU-gettext detection
|
||||||
|
//! (appending `gettext` to the manifest's Build-Depends).
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use super::meson::hello_c;
|
use super::{ProbeResult, TemplateHooks, source_dir_of};
|
||||||
use super::{OutputFile, ProbeResult, Template, source_dir_of};
|
use crate::new::options::NewOptions;
|
||||||
use crate::new::options::{NewOptions, TemplateId};
|
|
||||||
|
|
||||||
/// C/C++ with Autotools (`configure.ac`).
|
/// The logic half of the autotools template.
|
||||||
pub struct Autotools;
|
pub struct Hooks;
|
||||||
|
|
||||||
impl Template for Autotools {
|
/// The autotools template's hooks, registered in the template registry.
|
||||||
fn id(&self) -> TemplateId {
|
pub static HOOKS: Hooks = Hooks;
|
||||||
TemplateId::Autotools
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal `configure.ac`, the matching `Makefile.am` and `hello.c`.
|
impl TemplateHooks for Hooks {
|
||||||
/// The first source build runs `autoreconf` (integrated in the dh
|
/// A packaged `configure.ac` that sets up GNU gettext (the
|
||||||
/// sequence), so no generated configure script is committed.
|
/// `AM_GNU_GETTEXT` macro, see [`uses_gettext`]) appends `gettext` to
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
/// the manifest's Build-Depends.
|
||||||
vec![
|
fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
|
||||||
OutputFile::new(
|
|
||||||
"configure.ac",
|
|
||||||
format!(
|
|
||||||
"AC_INIT([{name}], [{version}])\n\
|
|
||||||
AM_INIT_AUTOMAKE([foreign])\n\
|
|
||||||
AC_PROG_CC\n\
|
|
||||||
AC_CONFIG_FILES([Makefile])\n\
|
|
||||||
AC_OUTPUT\n",
|
|
||||||
name = opts.name,
|
|
||||||
version = opts.upstream_version,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutputFile::new(
|
|
||||||
"Makefile.am",
|
|
||||||
format!(
|
|
||||||
"bin_PROGRAMS = {command}\n\
|
|
||||||
{command}_SOURCES = hello.c\n",
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
hello_c(opts),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No extra debian/ files: plain `dh $@` auto-detects `configure.ac`.
|
|
||||||
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
|
|
||||||
let mut deps = vec![
|
|
||||||
"autoconf".to_string(),
|
|
||||||
"automake".to_string(),
|
|
||||||
"libtool".to_string(),
|
|
||||||
];
|
|
||||||
if uses_gettext(opts) {
|
if uses_gettext(opts) {
|
||||||
deps.push("gettext".to_string());
|
base.push("gettext".to_string());
|
||||||
}
|
}
|
||||||
deps
|
base
|
||||||
}
|
|
||||||
|
|
||||||
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
|
||||||
"any"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Package name and version from the `AC_INIT` macro of `configure.ac`.
|
/// Package name and version from the `AC_INIT` macro of `configure.ac`.
|
||||||
@@ -109,13 +72,13 @@ fn uses_gettext(opts: &NewOptions) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts() -> NewOptions {
|
fn opts() -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Autotools,
|
template: TemplateId::AUTOTOOLS,
|
||||||
source_dir: SourceDir::Skeleton,
|
source_dir: SourceDir::Skeleton,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -141,7 +104,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn autotools_template_shape() {
|
fn autotools_template_shape() {
|
||||||
let o = opts();
|
let o = opts();
|
||||||
let template = super::super::get(TemplateId::Autotools).unwrap();
|
let template = super::super::get(TemplateId::AUTOTOOLS).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "any");
|
assert_eq!(template.architecture(&o), "any");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -156,28 +119,50 @@ mod tests {
|
|||||||
assert!(template.rules_extra(&o).is_empty());
|
assert!(template.rules_extra(&o).is_empty());
|
||||||
assert!(template.debian(&o).is_empty());
|
assert!(template.debian(&o).is_empty());
|
||||||
|
|
||||||
|
// Skeleton (manifest data): configure.ac + Makefile.am + hello.c,
|
||||||
|
// byte-exact.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
|
assert_eq!(skeleton.len(), 3);
|
||||||
let configure = skeleton
|
let configure = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.path == "configure.ac")
|
.find(|f| f.path == "configure.ac")
|
||||||
.expect("configure.ac skeleton");
|
.expect("configure.ac skeleton");
|
||||||
assert!(
|
assert_eq!(
|
||||||
configure
|
configure.contents,
|
||||||
.contents
|
"AC_INIT([mytool], [0.1.0])\n\
|
||||||
.starts_with("AC_INIT([mytool], [0.1.0])\n")
|
AM_INIT_AUTOMAKE([foreign])\n\
|
||||||
|
AC_PROG_CC\n\
|
||||||
|
AC_CONFIG_FILES([Makefile])\n\
|
||||||
|
AC_OUTPUT\n"
|
||||||
);
|
);
|
||||||
let makefile_am = skeleton
|
let makefile_am = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.path == "Makefile.am")
|
.find(|f| f.path == "Makefile.am")
|
||||||
.expect("Makefile.am skeleton");
|
.expect("Makefile.am skeleton");
|
||||||
assert!(makefile_am.contents.contains("bin_PROGRAMS = mytool"));
|
assert_eq!(
|
||||||
assert!(makefile_am.contents.contains("mytool_SOURCES = hello.c"));
|
makefile_am.contents,
|
||||||
assert!(skeleton.iter().any(|f| f.path == "hello.c"));
|
"bin_PROGRAMS = mytool\nmytool_SOURCES = hello.c\n"
|
||||||
|
);
|
||||||
|
let hello = skeleton
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.path == "hello.c")
|
||||||
|
.expect("hello.c skeleton");
|
||||||
|
assert_eq!(
|
||||||
|
hello.contents,
|
||||||
|
"#include <stdio.h>\n\
|
||||||
|
\n\
|
||||||
|
/* Placeholder for mytool, generated by `pkh new`. */\n\
|
||||||
|
int main(void)\n\
|
||||||
|
{\n\
|
||||||
|
\tprintf(\"Hello from mytool!\\n\");\n\
|
||||||
|
\treturn 0;\n\
|
||||||
|
}\n"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn autotools_probe_reads_ac_init() {
|
fn autotools_probe_reads_ac_init() {
|
||||||
let template = super::super::get(TemplateId::Autotools).unwrap();
|
let template = super::super::get(TemplateId::AUTOTOOLS).unwrap();
|
||||||
|
|
||||||
// Bracketed form (the generated skeleton's own shape).
|
// Bracketed form (the generated skeleton's own shape).
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
@@ -222,7 +207,7 @@ mod tests {
|
|||||||
let o = opts();
|
let o = opts();
|
||||||
assert!(!uses_gettext(&o));
|
assert!(!uses_gettext(&o));
|
||||||
assert!(
|
assert!(
|
||||||
!super::super::get(TemplateId::Autotools)
|
!super::super::get(TemplateId::AUTOTOOLS)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.build_depends(&o)
|
.build_depends(&o)
|
||||||
.contains(&"gettext".to_string())
|
.contains(&"gettext".to_string())
|
||||||
@@ -241,7 +226,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(uses_gettext(&o));
|
assert!(uses_gettext(&o));
|
||||||
assert!(
|
assert!(
|
||||||
super::super::get(TemplateId::Autotools)
|
super::super::get(TemplateId::AUTOTOOLS)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.build_depends(&o)
|
.build_depends(&o)
|
||||||
.contains(&"gettext".to_string())
|
.contains(&"gettext".to_string())
|
||||||
|
|||||||
+45
-61
@@ -1,63 +1,33 @@
|
|||||||
//! The `cmake` template: a C/C++ project built with CMake through the
|
//! The `cmake` template: a C/C++ project built with CMake through the
|
||||||
//! debhelper cmake buildsystem.
|
//! debhelper cmake buildsystem.
|
||||||
|
//!
|
||||||
|
//! The metadata and the `CMakeLists.txt`/`hello.c` skeleton bodies are
|
||||||
|
//! manifest data (`data/templates/cmake/manifest.yml`); the logic half
|
||||||
|
//! here is the `project()` probe and the wizard's pkg-config opt-in
|
||||||
|
//! (appended to the manifest's Build-Depends).
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use super::meson::hello_c;
|
use super::{ProbeResult, TemplateHooks};
|
||||||
use super::{OutputFile, ProbeResult, Template};
|
use crate::new::options::NewOptions;
|
||||||
use crate::new::options::{NewOptions, TemplateId};
|
|
||||||
|
|
||||||
/// C/C++ with CMake (`CMakeLists.txt`).
|
/// The logic half of the cmake template.
|
||||||
pub struct Cmake;
|
pub struct Hooks;
|
||||||
|
|
||||||
impl Template for Cmake {
|
/// The cmake template's hooks, registered in the template registry.
|
||||||
fn id(&self) -> TemplateId {
|
pub static HOOKS: Hooks = Hooks;
|
||||||
TemplateId::Cmake
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal `CMakeLists.txt` (project declaration + one installed
|
impl TemplateHooks for Hooks {
|
||||||
/// executable) and the classic `hello.c`.
|
/// The wizard's pkg-config opt-in ([`NewOptions::pkg_config`], offered
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
/// when the project's build file hints at `pkg_check_modules` usage)
|
||||||
vec![
|
/// adds `pkg-config` to the manifest's Build-Depends.
|
||||||
OutputFile::new(
|
fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
|
||||||
"CMakeLists.txt",
|
|
||||||
format!(
|
|
||||||
"cmake_minimum_required(VERSION 3.16)\n\
|
|
||||||
project({name} VERSION {version})\n\
|
|
||||||
\n\
|
|
||||||
add_executable({command} hello.c)\n\
|
|
||||||
install(TARGETS {command} RUNTIME DESTINATION bin)\n",
|
|
||||||
name = opts.name,
|
|
||||||
version = opts.upstream_version,
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
hello_c(opts),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No extra debian/ files: debhelper's cmake buildsystem handles the
|
|
||||||
/// configure/build/install steps.
|
|
||||||
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
|
|
||||||
let mut deps = vec!["cmake".to_string()];
|
|
||||||
if opts.pkg_config {
|
if opts.pkg_config {
|
||||||
deps.push("pkg-config".to_string());
|
base.push("pkg-config".to_string());
|
||||||
}
|
}
|
||||||
deps
|
base
|
||||||
}
|
|
||||||
|
|
||||||
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
|
||||||
"any"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rules_dh_line(&self) -> String {
|
|
||||||
"dh $@ --buildsystem=cmake".to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Project name and version from the `project(<name> VERSION …)`
|
/// Project name and version from the `project(<name> VERSION …)`
|
||||||
@@ -84,13 +54,13 @@ impl Template for Cmake {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts() -> NewOptions {
|
fn opts() -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Cmake,
|
template: TemplateId::CMAKE,
|
||||||
source_dir: SourceDir::Skeleton,
|
source_dir: SourceDir::Skeleton,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -116,7 +86,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn cmake_template_shape() {
|
fn cmake_template_shape() {
|
||||||
let o = opts();
|
let o = opts();
|
||||||
let template = super::super::get(TemplateId::Cmake).unwrap();
|
let template = super::super::get(TemplateId::CMAKE).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "any");
|
assert_eq!(template.architecture(&o), "any");
|
||||||
assert_eq!(template.build_depends(&o), vec!["cmake".to_string()]);
|
assert_eq!(template.build_depends(&o), vec!["cmake".to_string()]);
|
||||||
@@ -124,27 +94,41 @@ mod tests {
|
|||||||
assert!(template.rules_extra(&o).is_empty());
|
assert!(template.rules_extra(&o).is_empty());
|
||||||
assert!(template.debian(&o).is_empty());
|
assert!(template.debian(&o).is_empty());
|
||||||
|
|
||||||
|
// Skeleton (manifest data): CMakeLists.txt + hello.c, byte-exact.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
|
assert_eq!(skeleton.len(), 2);
|
||||||
let cmakelists = skeleton
|
let cmakelists = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.path == "CMakeLists.txt")
|
.find(|f| f.path == "CMakeLists.txt")
|
||||||
.expect("CMakeLists.txt skeleton");
|
.expect("CMakeLists.txt skeleton");
|
||||||
assert!(
|
assert_eq!(
|
||||||
cmakelists
|
cmakelists.contents,
|
||||||
.contents
|
"cmake_minimum_required(VERSION 3.16)\n\
|
||||||
.contains("project(mytool VERSION 0.1.0)")
|
project(mytool VERSION 0.1.0)\n\
|
||||||
|
\n\
|
||||||
|
add_executable(mytool hello.c)\n\
|
||||||
|
install(TARGETS mytool RUNTIME DESTINATION bin)\n"
|
||||||
);
|
);
|
||||||
assert!(
|
let hello = skeleton
|
||||||
cmakelists
|
.iter()
|
||||||
.contents
|
.find(|f| f.path == "hello.c")
|
||||||
.contains("add_executable(mytool hello.c)")
|
.expect("hello.c skeleton");
|
||||||
|
assert_eq!(
|
||||||
|
hello.contents,
|
||||||
|
"#include <stdio.h>\n\
|
||||||
|
\n\
|
||||||
|
/* Placeholder for mytool, generated by `pkh new`. */\n\
|
||||||
|
int main(void)\n\
|
||||||
|
{\n\
|
||||||
|
\tprintf(\"Hello from mytool!\\n\");\n\
|
||||||
|
\treturn 0;\n\
|
||||||
|
}\n"
|
||||||
);
|
);
|
||||||
assert!(skeleton.iter().any(|f| f.path == "hello.c"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cmake_probe_reads_project_declaration() {
|
fn cmake_probe_reads_project_declaration() {
|
||||||
let template = super::super::get(TemplateId::Cmake).unwrap();
|
let template = super::super::get(TemplateId::CMAKE).unwrap();
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
//! The `empty` template: a metapackage or an empty base package with no
|
|
||||||
//! build system at all.
|
|
||||||
//!
|
|
||||||
//! One template with two flavors: a non-empty `Depends` list selects the
|
|
||||||
//! **metapackage** flavor (the canonical `Architecture: all`, nothing
|
|
||||||
//! compiled, the Depends list *is* the payload shape), while an empty list
|
|
||||||
//! selects the **empty base** — pure `dh $@` plumbing as a starting point
|
|
||||||
//! for hand-written rules.
|
|
||||||
|
|
||||||
use super::{OutputFile, Template};
|
|
||||||
use crate::new::options::NewOptions;
|
|
||||||
|
|
||||||
/// Metapackage / empty base (no build system).
|
|
||||||
pub struct Empty;
|
|
||||||
|
|
||||||
impl Template for Empty {
|
|
||||||
fn id(&self) -> crate::new::options::TemplateId {
|
|
||||||
crate::new::options::TemplateId::Empty
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No upstream files; just a stub `README` marking the tree as
|
|
||||||
/// intentionally empty.
|
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
vec![OutputFile::new(
|
|
||||||
"README",
|
|
||||||
format!(
|
|
||||||
"{} - empty base tree scaffolded by `pkh new`; there is \
|
|
||||||
intentionally no upstream build system here.\n",
|
|
||||||
opts.name
|
|
||||||
),
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No extra debian/ files: the metapackage `Depends` list is carried by
|
|
||||||
/// [`NewOptions::depends`] into the common `debian/control` rendering.
|
|
||||||
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::new::options::{License, SourceDir, TemplateId};
|
|
||||||
|
|
||||||
fn opts(depends: Vec<String>) -> NewOptions {
|
|
||||||
NewOptions {
|
|
||||||
name: "metapkg".into(),
|
|
||||||
template: TemplateId::Empty,
|
|
||||||
source_dir: SourceDir::Skeleton,
|
|
||||||
upstream_version: "0.1.0".into(),
|
|
||||||
revision: 1,
|
|
||||||
summary: "A metapackage".into(),
|
|
||||||
long_description: "A metapackage".into(),
|
|
||||||
homepage: None,
|
|
||||||
license: License::Custom("unknown".into()),
|
|
||||||
command: "ignored".into(),
|
|
||||||
maintainer: ("Jane".into(), "jane@example.com".into()),
|
|
||||||
dist: "debian".into(),
|
|
||||||
series: "sid".into(),
|
|
||||||
release: false,
|
|
||||||
depends,
|
|
||||||
source_format: crate::new::options::SourceFormat::Quilt,
|
|
||||||
orig: Some(crate::new::options::OrigOrigin::Snapshot),
|
|
||||||
git: true,
|
|
||||||
autopkgtest: false,
|
|
||||||
pkg_config: false,
|
|
||||||
watch: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_template_shape() {
|
|
||||||
let template = super::super::get(TemplateId::Empty).unwrap();
|
|
||||||
|
|
||||||
// Metapackage flavor: the depends list travels in the options.
|
|
||||||
let o = opts(vec!["hello".into(), "hello-data (>= 1.0)".into()]);
|
|
||||||
assert!(template.debian(&o).is_empty());
|
|
||||||
assert_eq!(template.architecture(&o), "all");
|
|
||||||
assert!(template.build_depends(&o).is_empty());
|
|
||||||
assert!(template.rules_extra(&o).is_empty());
|
|
||||||
|
|
||||||
let skeleton = template.skeleton(&o);
|
|
||||||
assert_eq!(skeleton.len(), 1);
|
|
||||||
assert_eq!(skeleton[0].path, "README");
|
|
||||||
assert!(!skeleton[0].executable);
|
|
||||||
assert!(skeleton[0].contents.contains("metapkg"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+25
-68
@@ -1,84 +1,40 @@
|
|||||||
//! The `go` template: a Go module built through dh-golang.
|
//! The `go` template: a Go module built through dh-golang.
|
||||||
//!
|
//!
|
||||||
//! The source stanza carries `XS-Go-Import-Path`, probed from the `module`
|
//! The skeleton bodies (`go.mod`, `main.go`) are manifest data
|
||||||
//! line of `go.mod` when the packaged tree has one, defaulting to the
|
//! (`data/templates/go/manifest.yml`); the source stanza carries
|
||||||
//! package name (fresh skeletons embed the package name in their own
|
//! `XS-Go-Import-Path`, declared as the `{go_import_path}` placeholder by
|
||||||
//! `go.mod`).
|
//! the manifest and filled here from the `module` line of `go.mod` when
|
||||||
|
//! the packaged tree has one, defaulting to the package name (fresh
|
||||||
|
//! skeletons embed the package name in their own `go.mod`). The module
|
||||||
|
//! line is also the probe of an existing project.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::{OutputFile, Template, source_dir_of};
|
use super::{ProbeResult, TemplateHooks, source_dir_of};
|
||||||
use crate::new::options::{NewOptions, TemplateId};
|
use crate::new::options::NewOptions;
|
||||||
|
|
||||||
/// Go module (`go.mod`).
|
/// The logic half of the go template.
|
||||||
pub struct Go;
|
pub struct Hooks;
|
||||||
|
|
||||||
impl Template for Go {
|
/// The go template's hooks, registered in the template registry.
|
||||||
fn id(&self) -> TemplateId {
|
pub static HOOKS: Hooks = Hooks;
|
||||||
TemplateId::Go
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A stdlib-only `main.go` (no archive dependencies needed to build) and
|
impl TemplateHooks for Hooks {
|
||||||
/// the matching `go.mod` whose module path is the package name.
|
/// The `{go_import_path}` value of the manifest's `XS-Go-Import-Path`
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
/// source field (see [`import_path`]).
|
||||||
vec![
|
fn context(&self, opts: &NewOptions) -> Vec<(String, String)> {
|
||||||
OutputFile::new(
|
vec![("go_import_path".to_string(), import_path(opts))]
|
||||||
"go.mod",
|
|
||||||
format!(
|
|
||||||
"module {name}\n\
|
|
||||||
\n\
|
|
||||||
go 1.21\n",
|
|
||||||
name = opts.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutputFile::new(
|
|
||||||
"main.go",
|
|
||||||
format!(
|
|
||||||
"// Placeholder for {name}, generated by `pkh new`.\n\
|
|
||||||
package main\n\
|
|
||||||
\n\
|
|
||||||
import \"fmt\"\n\
|
|
||||||
\n\
|
|
||||||
func main() {{\n\
|
|
||||||
\tfmt.Println(\"Hello from {command}!\")\n\
|
|
||||||
}}\n",
|
|
||||||
name = opts.name,
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No extra debian/ files: dh-golang drives the build.
|
|
||||||
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> {
|
|
||||||
vec!["golang-any".to_string(), "dh-golang".to_string()]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
|
||||||
"any"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rules_dh_line(&self) -> String {
|
|
||||||
"dh $@ --buildsystem=golang".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_fields(&self, opts: &NewOptions) -> Vec<(String, String)> {
|
|
||||||
vec![("XS-Go-Import-Path".to_string(), import_path(opts))]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name (and default command) from the `module` line of `go.mod`: the
|
/// Name (and default command) from the `module` line of `go.mod`: the
|
||||||
/// last path segment is the conventional binary/package name.
|
/// last path segment is the conventional binary/package name.
|
||||||
fn probe(&self, dir: &Path) -> Option<super::ProbeResult> {
|
fn probe(&self, dir: &Path) -> Option<ProbeResult> {
|
||||||
let module = read_module_line(dir)?;
|
let module = read_module_line(dir)?;
|
||||||
let name = module.rsplit('/').next()?.to_string();
|
let name = module.rsplit('/').next()?.to_string();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(super::ProbeResult {
|
Some(ProbeResult {
|
||||||
name: Some(name.clone()),
|
name: Some(name.clone()),
|
||||||
command: Some(name),
|
command: Some(name),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -114,13 +70,13 @@ fn read_module_line(dir: &Path) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts(source_dir: SourceDir) -> NewOptions {
|
fn opts(source_dir: SourceDir) -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Go,
|
template: TemplateId::GO,
|
||||||
source_dir,
|
source_dir,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -146,7 +102,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn go_template_shape() {
|
fn go_template_shape() {
|
||||||
let o = opts(SourceDir::Skeleton);
|
let o = opts(SourceDir::Skeleton);
|
||||||
let template = super::super::get(TemplateId::Go).unwrap();
|
let template = super::super::get(TemplateId::GO).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "any");
|
assert_eq!(template.architecture(&o), "any");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -156,6 +112,7 @@ mod tests {
|
|||||||
assert_eq!(template.rules_dh_line(), "dh $@ --buildsystem=golang");
|
assert_eq!(template.rules_dh_line(), "dh $@ --buildsystem=golang");
|
||||||
assert!(template.debian(&o).is_empty());
|
assert!(template.debian(&o).is_empty());
|
||||||
|
|
||||||
|
// The skeleton bodies are manifest data now.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
assert!(
|
assert!(
|
||||||
skeleton
|
skeleton
|
||||||
@@ -180,7 +137,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let template = super::super::get(TemplateId::Go).unwrap();
|
let template = super::super::get(TemplateId::GO).unwrap();
|
||||||
let probe = template.probe(dir.path()).expect("probe result");
|
let probe = template.probe(dir.path()).expect("probe result");
|
||||||
assert_eq!(probe.name.as_deref(), Some("mytool"));
|
assert_eq!(probe.name.as_deref(), Some("mytool"));
|
||||||
assert_eq!(probe.command.as_deref(), Some("mytool"));
|
assert_eq!(probe.command.as_deref(), Some("mytool"));
|
||||||
|
|||||||
@@ -1,70 +1,40 @@
|
|||||||
//! The `makefile` template: a generic project driven by a plain `Makefile`.
|
//! The `makefile` template: a generic project driven by a plain Makefile.
|
||||||
//!
|
//!
|
||||||
//! debhelper's makefile buildsystem runs `make` for the build and
|
//! debhelper's makefile buildsystem runs `make` for the build and `make
|
||||||
//! `make install DESTDIR=...` when the Makefile carries an `install:` target
|
//! install DESTDIR=...` when the Makefile carries an `install:` target
|
||||||
//! (missing targets are skipped gracefully), so plain `dh $@` plumbing is
|
//! (missing targets are skipped gracefully), so plain `dh $@` plumbing is
|
||||||
//! enough here.
|
//! enough here. Everything is manifest data
|
||||||
|
//! (`data/templates/makefile/manifest.yml`: the `hello.c`/`Makefile`
|
||||||
|
//! skeleton and the skeleton-only `debian/install` mapping) except the
|
||||||
|
//! hint logged here for an existing tree, which probes its Makefile for a
|
||||||
|
//! phony `install:` target to tell whether `dh_auto_install` will run
|
||||||
|
//! `make install` (a skeleton's target is known by construction, so
|
||||||
|
//! skeletons need no code).
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::{OutputFile, Template, source_dir_of};
|
use super::TemplateHooks;
|
||||||
use crate::new::options::{NewOptions, SourceDir, TemplateId};
|
use crate::new::options::{NewOptions, SourceDir};
|
||||||
|
|
||||||
/// Generic Makefile-based project.
|
/// The logic half of the makefile template.
|
||||||
pub struct Makefile;
|
pub struct Hooks;
|
||||||
|
|
||||||
impl Template for Makefile {
|
/// The makefile template's hooks, registered in the template registry.
|
||||||
fn id(&self) -> TemplateId {
|
pub static HOOKS: Hooks = Hooks;
|
||||||
TemplateId::Makefile
|
|
||||||
|
impl TemplateHooks for Hooks {
|
||||||
|
/// No files of its own: the manifest carries the skeleton-only
|
||||||
|
/// `debian/install` mapping. When packaging an existing tree, probe
|
||||||
|
/// its Makefile for a phony `install:` target and say which install
|
||||||
|
/// step `dh_auto_install` will take (no `debian/install` is emitted
|
||||||
|
/// there — the source-relative mapping of an unknown artifact is only
|
||||||
|
/// the project's to write, and `make install` already ran).
|
||||||
|
fn debian_files(&self, opts: &NewOptions) -> Vec<super::OutputFile> {
|
||||||
|
if matches!(opts.source_dir, SourceDir::Skeleton) {
|
||||||
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
let dir = super::source_dir_of(opts);
|
||||||
/// A `hello.c` plus a `Makefile` with `all`/`install`/`clean` targets;
|
if let Some(dir) = dir.as_deref()
|
||||||
/// `install` honors `DESTDIR` and copies the binary to
|
|
||||||
/// `$(DESTDIR)/usr/bin/`.
|
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
vec![
|
|
||||||
super::meson::hello_c(opts),
|
|
||||||
OutputFile::new(
|
|
||||||
"Makefile",
|
|
||||||
format!(
|
|
||||||
"CC ?= cc\n\
|
|
||||||
CFLAGS ?= -O2 -Wall -Wextra\n\
|
|
||||||
PREFIX ?= /usr\n\
|
|
||||||
\n\
|
|
||||||
all: {command}\n\
|
|
||||||
\n\
|
|
||||||
{command}: hello.c\n\
|
|
||||||
\t$(CC) $(CFLAGS) -o $@ hello.c\n\
|
|
||||||
\n\
|
|
||||||
install: {command}\n\
|
|
||||||
\tinstall -Dm755 {command} $(DESTDIR)$(PREFIX)/bin/{command}\n\
|
|
||||||
\n\
|
|
||||||
clean:\n\
|
|
||||||
\trm -f {command}\n\
|
|
||||||
\n\
|
|
||||||
.PHONY: all install clean\n",
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `debian/install` mapping the built binary into `/usr/bin`, generated
|
|
||||||
/// only when the packaged Makefile carries a phony `install:` target:
|
|
||||||
/// for skeletons that is known by construction; when packaging an
|
|
||||||
/// existing tree the Makefile is probed instead (no `debian/install` is
|
|
||||||
/// emitted there — the source-relative mapping of an unknown artifact is
|
|
||||||
/// only the project's to write, and `make install` already ran).
|
|
||||||
fn debian(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
match &opts.source_dir {
|
|
||||||
SourceDir::Skeleton => {
|
|
||||||
vec![OutputFile::new(
|
|
||||||
"debian/install",
|
|
||||||
format!("{} usr/bin/{}\n", opts.command, opts.command),
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if let Some(dir) = source_dir_of(opts)
|
|
||||||
&& phony_install_target(&dir.join("Makefile")).is_some()
|
&& phony_install_target(&dir.join("Makefile")).is_some()
|
||||||
{
|
{
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -81,16 +51,6 @@ impl Template for Makefile {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> {
|
|
||||||
vec!["build-essential".to_string()]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
|
||||||
"any"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The name of the phony `install:` target of the Makefile at `path`, when
|
/// The name of the phony `install:` target of the Makefile at `path`, when
|
||||||
/// there is one: an unindented `install:` rule whose name also appears in a
|
/// there is one: an unindented `install:` rule whose name also appears in a
|
||||||
@@ -119,13 +79,13 @@ pub fn phony_install_target(path: &Path) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts(source_dir: SourceDir) -> NewOptions {
|
fn opts(source_dir: SourceDir) -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Makefile,
|
template: TemplateId::MAKEFILE,
|
||||||
source_dir,
|
source_dir,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -151,7 +111,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn makefile_template_shape() {
|
fn makefile_template_shape() {
|
||||||
let o = opts(SourceDir::Skeleton);
|
let o = opts(SourceDir::Skeleton);
|
||||||
let template = super::super::get(TemplateId::Makefile).unwrap();
|
let template = super::super::get(TemplateId::MAKEFILE).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "any");
|
assert_eq!(template.architecture(&o), "any");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -161,8 +121,8 @@ mod tests {
|
|||||||
assert_eq!(template.rules_dh_line(), "dh $@");
|
assert_eq!(template.rules_dh_line(), "dh $@");
|
||||||
assert!(template.rules_extra(&o).is_empty());
|
assert!(template.rules_extra(&o).is_empty());
|
||||||
|
|
||||||
// Skeleton: hello.c + Makefile with all/install/clean, and the
|
// Skeleton (manifest data): hello.c + Makefile with all/install/clean
|
||||||
// phony install target maps to debian/install.
|
// targets, and the phony install target maps to debian/install.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
assert!(skeleton.iter().any(|f| f.path == "hello.c"));
|
assert!(skeleton.iter().any(|f| f.path == "hello.c"));
|
||||||
let makefile = skeleton
|
let makefile = skeleton
|
||||||
@@ -187,7 +147,7 @@ mod tests {
|
|||||||
assert_eq!(debian[0].path, "debian/install");
|
assert_eq!(debian[0].path, "debian/install");
|
||||||
assert_eq!(debian[0].contents, "mytool usr/bin/mytool\n");
|
assert_eq!(debian[0].contents, "mytool usr/bin/mytool\n");
|
||||||
|
|
||||||
// Existing tree: nothing is emitted (probe log only).
|
// Existing tree: nothing is emitted (the probe log only).
|
||||||
let o = opts(SourceDir::Here);
|
let o = opts(SourceDir::Here);
|
||||||
assert!(template.debian(&o).is_empty());
|
assert!(template.debian(&o).is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-78
@@ -1,62 +1,33 @@
|
|||||||
//! The `meson` template: a C/C++ project built with Meson through the
|
//! The `meson` template: a C/C++ project built with Meson through the
|
||||||
//! debhelper meson buildsystem.
|
//! debhelper meson buildsystem.
|
||||||
|
//!
|
||||||
|
//! The metadata and the `meson.build`/`hello.c` skeleton bodies are
|
||||||
|
//! manifest data (`data/templates/meson/manifest.yml`); the logic half
|
||||||
|
//! here is the `project()` probe and the wizard's pkg-config opt-in
|
||||||
|
//! (appended to the manifest's Build-Depends).
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use super::{OutputFile, ProbeResult, Template};
|
use super::{ProbeResult, TemplateHooks};
|
||||||
use crate::new::options::{NewOptions, TemplateId};
|
use crate::new::options::NewOptions;
|
||||||
|
|
||||||
/// C/C++ with Meson (`meson.build`).
|
/// The logic half of the meson template.
|
||||||
pub struct Meson;
|
pub struct Hooks;
|
||||||
|
|
||||||
impl Template for Meson {
|
/// The meson template's hooks, registered in the template registry.
|
||||||
fn id(&self) -> TemplateId {
|
pub static HOOKS: Hooks = Hooks;
|
||||||
TemplateId::Meson
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal `meson.build` (project declaration + one installed
|
impl TemplateHooks for Hooks {
|
||||||
/// executable) and the classic `hello.c`.
|
/// The wizard's pkg-config opt-in ([`NewOptions::pkg_config`], offered
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
/// when the project's build file hints at `dependency(` usage) adds
|
||||||
vec![
|
/// `pkg-config` to the manifest's Build-Depends.
|
||||||
OutputFile::new(
|
fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
|
||||||
"meson.build",
|
|
||||||
format!(
|
|
||||||
"project('{name}', version: '{version}', license: '{license}', \
|
|
||||||
default_options: ['c_std=c11'])\n\
|
|
||||||
\n\
|
|
||||||
executable('{command}', 'hello.c', install: true)\n",
|
|
||||||
name = opts.name,
|
|
||||||
version = opts.upstream_version,
|
|
||||||
license = opts.license.spdx(),
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
hello_c(opts),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No extra debian/ files: debhelper's meson buildsystem handles the
|
|
||||||
/// configure/build/install steps.
|
|
||||||
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
|
|
||||||
let mut deps = vec!["meson".to_string()];
|
|
||||||
if opts.pkg_config {
|
if opts.pkg_config {
|
||||||
deps.push("pkg-config".to_string());
|
base.push("pkg-config".to_string());
|
||||||
}
|
}
|
||||||
deps
|
base
|
||||||
}
|
|
||||||
|
|
||||||
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
|
||||||
"any"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rules_dh_line(&self) -> String {
|
|
||||||
"dh $@ --buildsystem=meson".to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Project name and version from the `project('name', version: …)`
|
/// Project name and version from the `project('name', version: …)`
|
||||||
@@ -76,35 +47,16 @@ impl Template for Meson {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The shared `hello.c` placeholder of the C/C++ skeletons.
|
|
||||||
pub(super) fn hello_c(opts: &NewOptions) -> OutputFile {
|
|
||||||
OutputFile::new(
|
|
||||||
"hello.c",
|
|
||||||
format!(
|
|
||||||
"#include <stdio.h>\n\
|
|
||||||
\n\
|
|
||||||
/* Placeholder for {name}, generated by `pkh new`. */\n\
|
|
||||||
int main(void)\n\
|
|
||||||
{{\n\
|
|
||||||
\tprintf(\"Hello from {command}!\\n\");\n\
|
|
||||||
\treturn 0;\n\
|
|
||||||
}}\n",
|
|
||||||
name = opts.name,
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts() -> NewOptions {
|
fn opts() -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Meson,
|
template: TemplateId::MESON,
|
||||||
source_dir: SourceDir::Skeleton,
|
source_dir: SourceDir::Skeleton,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -130,7 +82,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn meson_template_shape() {
|
fn meson_template_shape() {
|
||||||
let o = opts();
|
let o = opts();
|
||||||
let template = super::super::get(TemplateId::Meson).unwrap();
|
let template = super::super::get(TemplateId::MESON).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "any");
|
assert_eq!(template.architecture(&o), "any");
|
||||||
assert_eq!(template.build_depends(&o), vec!["meson".to_string()]);
|
assert_eq!(template.build_depends(&o), vec!["meson".to_string()]);
|
||||||
@@ -138,24 +90,40 @@ mod tests {
|
|||||||
assert!(template.rules_extra(&o).is_empty());
|
assert!(template.rules_extra(&o).is_empty());
|
||||||
assert!(template.debian(&o).is_empty());
|
assert!(template.debian(&o).is_empty());
|
||||||
|
|
||||||
|
// Skeleton (manifest data): meson.build + hello.c, byte-exact.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
|
assert_eq!(skeleton.len(), 2);
|
||||||
let meson_build = skeleton
|
let meson_build = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.path == "meson.build")
|
.find(|f| f.path == "meson.build")
|
||||||
.expect("meson.build skeleton");
|
.expect("meson.build skeleton");
|
||||||
assert!(meson_build.contents.contains("project('mytool'"));
|
assert_eq!(
|
||||||
assert!(meson_build.contents.contains("version: '0.1.0'"));
|
meson_build.contents,
|
||||||
assert!(
|
"project('mytool', version: '0.1.0', license: 'MIT', \
|
||||||
meson_build
|
default_options: ['c_std=c11'])\n\
|
||||||
.contents
|
\n\
|
||||||
.contains("executable('mytool', 'hello.c', install: true)")
|
executable('mytool', 'hello.c', install: true)\n"
|
||||||
|
);
|
||||||
|
let hello = skeleton
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.path == "hello.c")
|
||||||
|
.expect("hello.c skeleton");
|
||||||
|
assert_eq!(
|
||||||
|
hello.contents,
|
||||||
|
"#include <stdio.h>\n\
|
||||||
|
\n\
|
||||||
|
/* Placeholder for mytool, generated by `pkh new`. */\n\
|
||||||
|
int main(void)\n\
|
||||||
|
{\n\
|
||||||
|
\tprintf(\"Hello from mytool!\\n\");\n\
|
||||||
|
\treturn 0;\n\
|
||||||
|
}\n"
|
||||||
);
|
);
|
||||||
assert!(skeleton.iter().any(|f| f.path == "hello.c"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn meson_probe_reads_project_declaration() {
|
fn meson_probe_reads_project_declaration() {
|
||||||
let template = super::super::get(TemplateId::Meson).unwrap();
|
let template = super::super::get(TemplateId::MESON).unwrap();
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@@ -181,7 +149,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pkg_config_opt_in_extends_build_depends() {
|
fn pkg_config_opt_in_extends_build_depends() {
|
||||||
let template = super::super::get(TemplateId::Meson).unwrap();
|
let template = super::super::get(TemplateId::MESON).unwrap();
|
||||||
let mut o = opts();
|
let mut o = opts();
|
||||||
assert_eq!(template.build_depends(&o), vec!["meson".to_string()]);
|
assert_eq!(template.build_depends(&o), vec!["meson".to_string()]);
|
||||||
o.pkg_config = true;
|
o.pkg_config = true;
|
||||||
|
|||||||
+1046
-215
File diff suppressed because it is too large
Load Diff
+79
-92
@@ -4,17 +4,25 @@
|
|||||||
//! …`) with a deliberately minimal line-oriented reader (see
|
//! …`) with a deliberately minimal line-oriented reader (see
|
||||||
//! [`read_pyproject`]) — pkh has no TOML dependency, and only a handful of
|
//! [`read_pyproject`]) — pkh has no TOML dependency, and only a handful of
|
||||||
//! keys matter here. A bare `setup.py`/`setup.cfg` project falls back to
|
//! keys matter here. A bare `setup.py`/`setup.cfg` project falls back to
|
||||||
//! setuptools without the `pybuild-plugin-pyproject` helper.
|
//! setuptools without the `pybuild-plugin-pyproject` helper. The skeleton
|
||||||
|
//! bodies are manifest data (`data/templates/python/manifest.yml`),
|
||||||
|
//! named by the `{module_name}` placeholder this module derives from the
|
||||||
|
//! package name; the manifest's Build-Depends/architecture are the
|
||||||
|
//! fresh-skeleton baseline the hooks here resolve for an existing project
|
||||||
|
//! (backend package, pyproject presence, C-extension hints).
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use super::{OutputFile, ProbeResult, Template, source_dir_of};
|
use super::{ProbeResult, TemplateHooks, source_dir_of};
|
||||||
use crate::new::options::{NewOptions, TemplateId};
|
use crate::new::options::NewOptions;
|
||||||
|
|
||||||
/// Python project (`pyproject.toml` / `setup.py` / `setup.cfg`).
|
/// The logic half of the python template.
|
||||||
pub struct Python;
|
pub struct Hooks;
|
||||||
|
|
||||||
|
/// The python template's hooks, registered in the template registry.
|
||||||
|
pub static HOOKS: Hooks = Hooks;
|
||||||
|
|
||||||
/// The PEP 517 backend of a project.
|
/// The PEP 517 backend of a project.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -113,86 +121,45 @@ fn c_extension_hints(opts: &NewOptions) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template for Python {
|
impl TemplateHooks for Hooks {
|
||||||
fn id(&self) -> TemplateId {
|
/// The `{module_name}` value of the manifest's skeleton bodies (see
|
||||||
TemplateId::Python
|
/// [`module_name`]): the derived Python identifier naming the skeleton
|
||||||
|
/// package directory and the console-script entry point of
|
||||||
|
/// `pyproject.toml`.
|
||||||
|
fn context(&self, opts: &NewOptions) -> Vec<(String, String)> {
|
||||||
|
vec![("module_name".to_string(), module_name(opts))]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A minimal setuptools-based `pyproject.toml` with one console script,
|
/// Amend the manifest's Build-Depends (the fresh-skeleton baseline)
|
||||||
/// plus the one-module package providing it.
|
/// with what only the packaged project decides: C-extension hints add
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
/// `python3-all-dev` next to `python3-all`, a bare `setup.py` project
|
||||||
let module = module_name(opts);
|
/// drops the pyproject plugin, and the actual backend package replaces
|
||||||
vec![
|
/// the skeleton's setuptools entry.
|
||||||
OutputFile::new(
|
fn build_depends(&self, opts: &NewOptions, mut base: Vec<String>) -> Vec<String> {
|
||||||
"pyproject.toml",
|
|
||||||
format!(
|
|
||||||
"[build-system]\n\
|
|
||||||
requires = [\"setuptools\"]\n\
|
|
||||||
build-backend = \"setuptools.build_meta\"\n\
|
|
||||||
\n\
|
|
||||||
[project]\n\
|
|
||||||
name = \"{name}\"\n\
|
|
||||||
version = \"{version}\"\n\
|
|
||||||
description = \"{summary}\"\n\
|
|
||||||
requires-python = \">=3.8\"\n\
|
|
||||||
\n\
|
|
||||||
[project.scripts]\n\
|
|
||||||
{command} = \"{module}:main\"\n",
|
|
||||||
name = opts.name,
|
|
||||||
version = opts.upstream_version,
|
|
||||||
summary = opts.summary,
|
|
||||||
command = opts.command,
|
|
||||||
module = module,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutputFile::new(
|
|
||||||
format!("{module}/__init__.py"),
|
|
||||||
format!(
|
|
||||||
"\"\"\"Placeholder for {name}, generated by `pkh new`.\"\"\"\n\
|
|
||||||
\n\
|
|
||||||
\n\
|
|
||||||
def main() -> None:\n\
|
|
||||||
\x20 print(\"Hello from {command}!\")\n",
|
|
||||||
name = opts.name,
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No extra debian/ files: pybuild installs the package and its console
|
|
||||||
/// entry points (under `/usr/bin`) automatically.
|
|
||||||
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
|
|
||||||
let mut deps = vec!["dh-python".to_string(), "python3-all".to_string()];
|
|
||||||
if c_extension_hints(opts) {
|
if c_extension_hints(opts) {
|
||||||
deps.push("python3-all-dev".to_string());
|
let at = base
|
||||||
|
.iter()
|
||||||
|
.position(|dep| dep == "python3-all")
|
||||||
|
.map_or(base.len(), |at| at + 1);
|
||||||
|
base.insert(at, "python3-all-dev".to_string());
|
||||||
}
|
}
|
||||||
if uses_pyproject(opts) {
|
if !uses_pyproject(opts) {
|
||||||
deps.push("pybuild-plugin-pyproject".to_string());
|
base.retain(|dep| dep != "pybuild-plugin-pyproject");
|
||||||
}
|
}
|
||||||
deps.push(backend(opts).package().to_string());
|
// The backend package: the skeleton baseline ends in the generated
|
||||||
deps
|
// setuptools backend, which the packaged project's own replaces.
|
||||||
|
let backend_package = backend(opts).package().to_string();
|
||||||
|
match base.iter().position(|dep| dep == "python3-setuptools") {
|
||||||
|
Some(at) => base[at] = backend_package,
|
||||||
|
None => base.push(backend_package),
|
||||||
|
}
|
||||||
|
base
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `all` unless the project hints at compiled C extensions.
|
/// `any` instead of the manifest's `all` when the project hints at
|
||||||
fn architecture(&self, opts: &NewOptions) -> &'static str {
|
/// compiled C extensions.
|
||||||
if c_extension_hints(opts) {
|
fn architecture(&self, opts: &NewOptions) -> Option<&'static str> {
|
||||||
"any"
|
c_extension_hints(opts).then_some("any")
|
||||||
} else {
|
|
||||||
"all"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rules_dh_line(&self) -> String {
|
|
||||||
"dh $@ --with python3 --buildsystem=pybuild".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_fields(&self, _opts: &NewOptions) -> Vec<(String, String)> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata from the `[project]` section of `pyproject.toml` (name,
|
/// Metadata from the `[project]` section of `pyproject.toml` (name,
|
||||||
@@ -339,13 +306,13 @@ fn split_key_value(line: &str) -> Option<(&str, String)> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts(source_dir: SourceDir) -> NewOptions {
|
fn opts(source_dir: SourceDir) -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Python,
|
template: TemplateId::PYTHON,
|
||||||
source_dir,
|
source_dir,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -375,7 +342,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn python_skeleton_shape() {
|
fn python_skeleton_shape() {
|
||||||
let o = opts(SourceDir::Skeleton);
|
let o = opts(SourceDir::Skeleton);
|
||||||
let template = super::super::get(TemplateId::Python).unwrap();
|
let template = super::super::get(TemplateId::PYTHON).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "all");
|
assert_eq!(template.architecture(&o), "all");
|
||||||
assert!(template.debian(&o).is_empty());
|
assert!(template.debian(&o).is_empty());
|
||||||
@@ -384,23 +351,41 @@ mod tests {
|
|||||||
"dh $@ --with python3 --buildsystem=pybuild"
|
"dh $@ --with python3 --buildsystem=pybuild"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Skeleton (manifest data): pyproject.toml + the module package,
|
||||||
|
// byte-exact.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
|
assert_eq!(skeleton.len(), 2);
|
||||||
let pyproject = skeleton
|
let pyproject = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.path == "pyproject.toml")
|
.find(|f| f.path == "pyproject.toml")
|
||||||
.expect("pyproject skeleton");
|
.expect("pyproject skeleton");
|
||||||
assert!(
|
assert_eq!(
|
||||||
pyproject
|
pyproject.contents,
|
||||||
.contents
|
"[build-system]\n\
|
||||||
.contains("build-backend = \"setuptools.build_meta\"")
|
requires = [\"setuptools\"]\n\
|
||||||
|
build-backend = \"setuptools.build_meta\"\n\
|
||||||
|
\n\
|
||||||
|
[project]\n\
|
||||||
|
name = \"mytool\"\n\
|
||||||
|
version = \"0.1.0\"\n\
|
||||||
|
description = \"A tool\"\n\
|
||||||
|
requires-python = \">=3.8\"\n\
|
||||||
|
\n\
|
||||||
|
[project.scripts]\n\
|
||||||
|
mytool = \"mytool:main\"\n"
|
||||||
);
|
);
|
||||||
assert!(pyproject.contents.contains("name = \"mytool\""));
|
|
||||||
assert!(pyproject.contents.contains("mytool = \"mytool:main\""));
|
|
||||||
let module = skeleton
|
let module = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.path == "mytool/__init__.py")
|
.find(|f| f.path == "mytool/__init__.py")
|
||||||
.expect("module skeleton");
|
.expect("module skeleton");
|
||||||
assert!(module.contents.contains("def main()"));
|
assert_eq!(
|
||||||
|
module.contents,
|
||||||
|
"\"\"\"Placeholder for mytool, generated by `pkh new`.\"\"\"\n\
|
||||||
|
\n\
|
||||||
|
\n\
|
||||||
|
def main() -> None:\n\
|
||||||
|
\x20 print(\"Hello from mytool!\")\n"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// dpkg names may carry `+`/`.` and start with a digit — none of which a
|
/// dpkg names may carry `+`/`.` and start with a digit — none of which a
|
||||||
@@ -420,7 +405,7 @@ mod tests {
|
|||||||
assert_eq!(module_name(&o), "my_tool");
|
assert_eq!(module_name(&o), "my_tool");
|
||||||
|
|
||||||
// The skeleton module path and the pyproject script agree.
|
// The skeleton module path and the pyproject script agree.
|
||||||
let template = super::super::get(TemplateId::Python).unwrap();
|
let template = super::super::get(TemplateId::PYTHON).unwrap();
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
assert!(skeleton.iter().any(|f| f.path == "my_tool/__init__.py"));
|
assert!(skeleton.iter().any(|f| f.path == "my_tool/__init__.py"));
|
||||||
assert!(skeleton.iter().any(|f| {
|
assert!(skeleton.iter().any(|f| {
|
||||||
@@ -447,7 +432,7 @@ mod tests {
|
|||||||
// Unknown values fall back to setuptools.
|
// Unknown values fall back to setuptools.
|
||||||
assert_eq!(backend_of_value("mystery.backend"), Backend::Setuptools);
|
assert_eq!(backend_of_value("mystery.backend"), Backend::Setuptools);
|
||||||
|
|
||||||
let template = super::super::get(TemplateId::Python).unwrap();
|
let template = super::super::get(TemplateId::PYTHON).unwrap();
|
||||||
|
|
||||||
// No source dir (skeleton): setuptools + pyproject plugin.
|
// No source dir (skeleton): setuptools + pyproject plugin.
|
||||||
assert_eq!(backend(&opts(SourceDir::Skeleton)), Backend::Setuptools);
|
assert_eq!(backend(&opts(SourceDir::Skeleton)), Backend::Setuptools);
|
||||||
@@ -507,7 +492,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn c_extension_hints_flip_architecture_and_deps() {
|
fn c_extension_hints_flip_architecture_and_deps() {
|
||||||
let template = super::super::get(TemplateId::Python).unwrap();
|
let template = super::super::get(TemplateId::PYTHON).unwrap();
|
||||||
|
|
||||||
// pyo3 in Cargo.toml.
|
// pyo3 in Cargo.toml.
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
@@ -520,6 +505,7 @@ mod tests {
|
|||||||
assert!(c_extension_hints(&o));
|
assert!(c_extension_hints(&o));
|
||||||
let deps = template.build_depends(&o);
|
let deps = template.build_depends(&o);
|
||||||
assert!(deps.contains(&"python3-all-dev".to_string()), "{deps:?}");
|
assert!(deps.contains(&"python3-all-dev".to_string()), "{deps:?}");
|
||||||
|
assert_eq!(template.architecture(&o), "any");
|
||||||
|
|
||||||
// ext_modules in setup.py.
|
// ext_modules in setup.py.
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
@@ -537,11 +523,12 @@ mod tests {
|
|||||||
let o = source_opts(dir.path());
|
let o = source_opts(dir.path());
|
||||||
assert!(!c_extension_hints(&o));
|
assert!(!c_extension_hints(&o));
|
||||||
assert_eq!(template.build_depends(&o).len(), 4); // dh-python, python3-all, plugin, setuptools
|
assert_eq!(template.build_depends(&o).len(), 4); // dh-python, python3-all, plugin, setuptools
|
||||||
|
assert_eq!(template.architecture(&o), "all");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn probe_reads_project_and_scripts() {
|
fn probe_reads_project_and_scripts() {
|
||||||
let template = super::super::get(TemplateId::Python).unwrap();
|
let template = super::super::get(TemplateId::PYTHON).unwrap();
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
|
|||||||
+70
-121
@@ -9,16 +9,25 @@
|
|||||||
//! scaffold continues with a loud warning and reports
|
//! scaffold continues with a loud warning and reports
|
||||||
//! [`ScaffoldOutcome::vendoring_failed`] to the flow — the package will not
|
//! [`ScaffoldOutcome::vendoring_failed`] to the flow — the package will not
|
||||||
//! build until the user vendors manually.
|
//! build until the user vendors manually.
|
||||||
|
//!
|
||||||
|
//! The metadata and bodies are manifest data
|
||||||
|
//! (`data/templates/rust/manifest.yml`: the skeleton `Cargo.toml` /
|
||||||
|
//! `src/main.rs` and the `debian/rules` vendored-build overrides), rendered
|
||||||
|
//! through the placeholders this module supplies; the logic half here is
|
||||||
|
//! the project probe and the vendoring hook.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{OutputFile, ProbeResult, ScaffoldOutcome, Template, find_on_path, source_dir_of};
|
use super::{ProbeResult, ScaffoldOutcome, TemplateHooks, find_on_path, source_dir_of};
|
||||||
use crate::new::options::{NewOptions, SourceDir, TemplateId};
|
use crate::new::options::{NewOptions, SourceDir};
|
||||||
|
|
||||||
/// Rust project (`Cargo.toml`).
|
/// The logic half of the rust template.
|
||||||
pub struct Rust;
|
pub struct Hooks;
|
||||||
|
|
||||||
|
/// The rust template's hooks, registered in the template registry.
|
||||||
|
pub static HOOKS: Hooks = Hooks;
|
||||||
|
|
||||||
/// The source-replacement configuration, used when `cargo vendor` did not
|
/// The source-replacement configuration, used when `cargo vendor` did not
|
||||||
/// print one itself (old cargo versions, empty output).
|
/// print one itself (old cargo versions, empty output).
|
||||||
@@ -30,117 +39,30 @@ fn crate_name(opts: &NewOptions) -> String {
|
|||||||
opts.name.replace(['+', '.'], "_")
|
opts.name.replace(['+', '.'], "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template for Rust {
|
impl TemplateHooks for Hooks {
|
||||||
fn id(&self) -> TemplateId {
|
/// The placeholder values of the manifest bodies: `{crate_name}` names
|
||||||
TemplateId::Rust
|
/// the skeleton crate (a sanitized package name — see [`crate_name`]);
|
||||||
}
|
/// `{locked}` is ` --locked` only when the packaged tree already
|
||||||
|
/// carries a `Cargo.lock` (fresh skeletons have none yet — the
|
||||||
/// A zero-dependency `Cargo.toml` and the matching `src/main.rs`.
|
/// vendoring hook patches the flag in once `cargo vendor` created it);
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
/// `{artifact}` is the built binary of the rules install override —
|
||||||
vec![
|
/// a skeleton's is named after its crate, an existing project's under
|
||||||
OutputFile::new(
|
/// the (probed or answered) command.
|
||||||
"Cargo.toml",
|
fn context(&self, opts: &NewOptions) -> Vec<(String, String)> {
|
||||||
format!(
|
|
||||||
"[package]\n\
|
|
||||||
name = \"{name}\"\n\
|
|
||||||
version = \"{version}\"\n\
|
|
||||||
edition = \"2021\"\n\
|
|
||||||
\n\
|
|
||||||
[dependencies]\n",
|
|
||||||
name = crate_name(opts),
|
|
||||||
version = opts.upstream_version,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutputFile::new(
|
|
||||||
"src/main.rs",
|
|
||||||
format!(
|
|
||||||
"// Placeholder for {name}, generated by `pkh new`.\n\
|
|
||||||
fn main() {{\n\
|
|
||||||
\tprintln!(\"Hello from {command}!\");\n\
|
|
||||||
}}\n",
|
|
||||||
name = opts.name,
|
|
||||||
command = opts.command,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No extra debian/ files: the vendored build lives entirely in the
|
|
||||||
/// rules overrides.
|
|
||||||
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> {
|
|
||||||
vec!["cargo:native".to_string(), "rustc:native".to_string()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Root `.gitignore` entries contributed in every mode: the vendoring
|
|
||||||
/// hook runs unconditionally for this template, so `vendor/` and
|
|
||||||
/// `.cargo/config.toml` exist (or will exist) in every scaffolded rust
|
|
||||||
/// tree and must stay out of the repository. `.cargo/config.toml` is
|
|
||||||
/// the subtle one: it points cargo at `vendor/`, so committing it would
|
|
||||||
/// break plain `cargo build` for anyone cloning the repository without
|
|
||||||
/// the vendored sources.
|
|
||||||
fn gitignore_entries(&self, _opts: &NewOptions) -> Vec<String> {
|
|
||||||
vec!["vendor/".to_string(), ".cargo/config.toml".to_string()]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn architecture(&self, _opts: &NewOptions) -> &'static str {
|
|
||||||
"any"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The vendored build overrides. `--locked` is used only when the
|
|
||||||
/// packaged tree already carries a `Cargo.lock` (fresh skeletons have
|
|
||||||
/// none yet — the vendoring hook patches the flag in once `cargo vendor`
|
|
||||||
/// created it, see [`patch_rules_locked`]); omitting it is always safe.
|
|
||||||
/// The built artifact of a skeleton is named after its crate (a
|
|
||||||
/// sanitized package name) and installed under the command name.
|
|
||||||
///
|
|
||||||
/// `dh_update_autotools_config` is overridden away: crates embedding C
|
|
||||||
/// sources (e.g. `-sys` crates shipping `config.sub`/`config.guess`)
|
|
||||||
/// carry per-file cargo checksums, and debhelper refreshing those files
|
|
||||||
/// with the system's newer copies would break `cargo build --offline`.
|
|
||||||
/// `dh_clean` gets `-X Cargo.toml.orig` for the same reason: it treats
|
|
||||||
/// every vendored `Cargo.toml.orig` as a patch backup and deletes it,
|
|
||||||
/// which breaks the checksums on any build without a warm cache.
|
|
||||||
fn rules_extra(&self, opts: &NewOptions) -> String {
|
|
||||||
let locked = if lockfile_present(opts) {
|
let locked = if lockfile_present(opts) {
|
||||||
" --locked"
|
" --locked"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
// The skeleton's cargo artifact is the crate name; for an existing
|
|
||||||
// project the (probed or answered) command names the binary.
|
|
||||||
let artifact = match opts.source_dir {
|
let artifact = match opts.source_dir {
|
||||||
SourceDir::Skeleton => crate_name(opts),
|
SourceDir::Skeleton => crate_name(opts),
|
||||||
_ => opts.command.clone(),
|
_ => opts.command.clone(),
|
||||||
};
|
};
|
||||||
format!(
|
vec![
|
||||||
"override_dh_auto_build:\n\
|
("crate_name".to_string(), crate_name(opts)),
|
||||||
\tcargo build --release --offline{locked}\n\
|
("locked".to_string(), locked.to_string()),
|
||||||
\n\
|
("artifact".to_string(), artifact),
|
||||||
override_dh_auto_install:\n\
|
]
|
||||||
\tinstall -Dm755 target/release/{artifact} debian/{name}/usr/bin/{command}\n\
|
|
||||||
\n\
|
|
||||||
override_dh_auto_test:\n\
|
|
||||||
\tcargo test --release --offline{locked}\n\
|
|
||||||
\n\
|
|
||||||
override_dh_update_autotools_config:\n\
|
|
||||||
\n\
|
|
||||||
override_dh_clean:\n\
|
|
||||||
\t# dh_clean unlinks `*.orig` patch backups, but vendored crates\n\
|
|
||||||
\t# ship files like `Cargo.toml.orig` that cargo's per-file\n\
|
|
||||||
\t# checksums require on cold builds (chroots, Launchpad).\n\
|
|
||||||
\tdh_clean -X .orig\n\
|
|
||||||
\n\
|
|
||||||
override_dh_auto_clean:\n\
|
|
||||||
\tcargo clean\n",
|
|
||||||
locked = locked,
|
|
||||||
artifact = artifact,
|
|
||||||
command = opts.command,
|
|
||||||
name = opts.name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name, version, description, homepage, license and first binary from
|
/// Name, version, description, homepage, license and first binary from
|
||||||
@@ -501,14 +423,14 @@ fn parse_toolchain_channel(content: &str) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::new::options::{License, SourceDir};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts(source_dir: SourceDir) -> NewOptions {
|
fn opts(source_dir: SourceDir) -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Rust,
|
template: TemplateId::RUST,
|
||||||
source_dir,
|
source_dir,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -534,7 +456,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rust_template_shape() {
|
fn rust_template_shape() {
|
||||||
let o = opts(SourceDir::Skeleton);
|
let o = opts(SourceDir::Skeleton);
|
||||||
let template = super::super::get(TemplateId::Rust).unwrap();
|
let template = super::super::get(TemplateId::RUST).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "any");
|
assert_eq!(template.architecture(&o), "any");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -544,14 +466,33 @@ mod tests {
|
|||||||
assert_eq!(template.rules_dh_line(), "dh $@");
|
assert_eq!(template.rules_dh_line(), "dh $@");
|
||||||
assert!(template.debian(&o).is_empty());
|
assert!(template.debian(&o).is_empty());
|
||||||
|
|
||||||
// Skeleton: Cargo.toml + src/main.rs.
|
// Skeleton (manifest data): Cargo.toml + src/main.rs, byte-exact.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
assert!(
|
assert_eq!(skeleton.len(), 2);
|
||||||
skeleton
|
let cargo_toml = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.any(|f| f.path == "Cargo.toml" && f.contents.contains("name = \"mytool\""))
|
.find(|f| f.path == "Cargo.toml")
|
||||||
|
.expect("Cargo.toml skeleton");
|
||||||
|
assert_eq!(
|
||||||
|
cargo_toml.contents,
|
||||||
|
"[package]\n\
|
||||||
|
name = \"mytool\"\n\
|
||||||
|
version = \"0.1.0\"\n\
|
||||||
|
edition = \"2021\"\n\
|
||||||
|
\n\
|
||||||
|
[dependencies]\n"
|
||||||
|
);
|
||||||
|
let main_rs = skeleton
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.path == "src/main.rs")
|
||||||
|
.expect("src/main.rs skeleton");
|
||||||
|
assert_eq!(
|
||||||
|
main_rs.contents,
|
||||||
|
"// Placeholder for mytool, generated by `pkh new`.\n\
|
||||||
|
fn main() {\n\
|
||||||
|
\tprintln!(\"Hello from mytool!\");\n\
|
||||||
|
}\n"
|
||||||
);
|
);
|
||||||
assert!(skeleton.iter().any(|f| f.path == "src/main.rs"));
|
|
||||||
|
|
||||||
// Fresh skeleton: no Cargo.lock, so no --locked flag anywhere.
|
// Fresh skeleton: no Cargo.lock, so no --locked flag anywhere.
|
||||||
let extra = template.rules_extra(&o);
|
let extra = template.rules_extra(&o);
|
||||||
@@ -568,7 +509,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rules_use_locked_only_with_lockfile() {
|
fn rules_use_locked_only_with_lockfile() {
|
||||||
let template = super::super::get(TemplateId::Rust).unwrap();
|
let template = super::super::get(TemplateId::RUST).unwrap();
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
|
||||||
@@ -598,14 +539,22 @@ mod tests {
|
|||||||
command: "mytool".into(),
|
command: "mytool".into(),
|
||||||
..opts(SourceDir::Skeleton)
|
..opts(SourceDir::Skeleton)
|
||||||
};
|
};
|
||||||
let template = super::super::get(TemplateId::Rust).unwrap();
|
let template = super::super::get(TemplateId::RUST).unwrap();
|
||||||
|
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
let cargo_toml = skeleton
|
let cargo_toml = skeleton
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.path == "Cargo.toml")
|
.find(|f| f.path == "Cargo.toml")
|
||||||
.expect("Cargo.toml skeleton");
|
.expect("Cargo.toml skeleton");
|
||||||
assert!(cargo_toml.contents.contains("name = \"my_tool_\""));
|
assert_eq!(
|
||||||
|
cargo_toml.contents,
|
||||||
|
"[package]\n\
|
||||||
|
name = \"my_tool_\"\n\
|
||||||
|
version = \"0.1.0\"\n\
|
||||||
|
edition = \"2021\"\n\
|
||||||
|
\n\
|
||||||
|
[dependencies]\n"
|
||||||
|
);
|
||||||
|
|
||||||
let extra = template.rules_extra(&o);
|
let extra = template.rules_extra(&o);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -618,7 +567,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn probe_reads_cargo_toml_lines() {
|
fn probe_reads_cargo_toml_lines() {
|
||||||
let template = super::super::get(TemplateId::Rust).unwrap();
|
let template = super::super::get(TemplateId::RUST).unwrap();
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@@ -670,7 +619,7 @@ mod tests {
|
|||||||
std::fs::create_dir_all(dir.path().join("src")).unwrap();
|
std::fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||||
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}\n").unwrap();
|
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}\n").unwrap();
|
||||||
|
|
||||||
let template = super::super::get(TemplateId::Rust).unwrap();
|
let template = super::super::get(TemplateId::RUST).unwrap();
|
||||||
let probe = template.probe(dir.path()).expect("probe result");
|
let probe = template.probe(dir.path()).expect("probe result");
|
||||||
assert_eq!(probe.name.as_deref(), Some("metaprobe"));
|
assert_eq!(probe.name.as_deref(), Some("metaprobe"));
|
||||||
assert_eq!(probe.version.as_deref(), Some("0.9.0"));
|
assert_eq!(probe.version.as_deref(), Some("0.9.0"));
|
||||||
@@ -687,7 +636,7 @@ mod tests {
|
|||||||
fn post_write_vendors_skeleton() {
|
fn post_write_vendors_skeleton() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let o = opts(SourceDir::Skeleton);
|
let o = opts(SourceDir::Skeleton);
|
||||||
let template = super::super::get(TemplateId::Rust).unwrap();
|
let template = super::super::get(TemplateId::RUST).unwrap();
|
||||||
for file in template.skeleton(&o) {
|
for file in template.skeleton(&o) {
|
||||||
let path = dir.path().join(&file.path);
|
let path = dir.path().join(&file.path);
|
||||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
@@ -881,7 +830,7 @@ mod tests {
|
|||||||
/// no pin file leaves the field empty.
|
/// no pin file leaves the field empty.
|
||||||
#[test]
|
#[test]
|
||||||
fn probe_reports_the_toolchain_pin() {
|
fn probe_reports_the_toolchain_pin() {
|
||||||
let template = super::super::get(TemplateId::Rust).unwrap();
|
let template = super::super::get(TemplateId::RUST).unwrap();
|
||||||
let cargo_toml = "[package]\nname = \"pinned\"\nversion = \"1.0.0\"\n";
|
let cargo_toml = "[package]\nname = \"pinned\"\nversion = \"1.0.0\"\n";
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
|
|||||||
+30
-42
@@ -1,47 +1,25 @@
|
|||||||
//! The `shell` template: a single interpreted script installed to
|
//! The `shell` template: a single interpreted script installed to
|
||||||
//! `/usr/bin` with plain `dh $@` plumbing.
|
//! `/usr/bin` with plain `dh $@` plumbing.
|
||||||
|
//!
|
||||||
|
//! Everything except the probe is manifest data
|
||||||
|
//! (`data/templates/shell/manifest.yml`: the skeleton script, its
|
||||||
|
//! skeleton-only `debian/install` mapping, the plain `dh $@` plumbing).
|
||||||
|
//! The logic half here pre-fills the wizard answers from the file name of
|
||||||
|
//! the single top-level script — the same heuristic [`crate::new::detect`]
|
||||||
|
//! bases its shell detection on.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::{OutputFile, ProbeResult, Template};
|
use super::{ProbeResult, TemplateHooks};
|
||||||
use crate::new::options::{self, NewOptions, SourceDir};
|
use crate::new::options;
|
||||||
|
|
||||||
/// Shell script / single interpreted file.
|
/// The logic half of the shell template.
|
||||||
pub struct Shell;
|
pub struct Hooks;
|
||||||
|
|
||||||
impl Template for Shell {
|
/// The shell template's hooks, registered in the template registry.
|
||||||
fn id(&self) -> crate::new::options::TemplateId {
|
pub static HOOKS: Hooks = Hooks;
|
||||||
crate::new::options::TemplateId::Shell
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal executable script named after the command, with a `#!/bin/sh`
|
|
||||||
/// shebang and an `echo` placeholder.
|
|
||||||
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
vec![OutputFile::executable(
|
|
||||||
format!("{}.sh", opts.command),
|
|
||||||
format!(
|
|
||||||
"#!/bin/sh\n# Placeholder for {}, generated by `pkh new`.\n\
|
|
||||||
echo \"Hello from {}!\"\n",
|
|
||||||
opts.name, opts.command
|
|
||||||
),
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `debian/install` mapping the script into `/usr/bin/<command>`
|
|
||||||
/// (debian/install renames when the destination carries a file name).
|
|
||||||
/// Only for the skeleton mode: when packaging an existing tree the
|
|
||||||
/// generated mapping would reference the non-existent skeleton script,
|
|
||||||
/// so the user writes their own install file instead.
|
|
||||||
fn debian(&self, opts: &NewOptions) -> Vec<OutputFile> {
|
|
||||||
if !matches!(opts.source_dir, SourceDir::Skeleton) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
vec![OutputFile::new(
|
|
||||||
"debian/install",
|
|
||||||
format!("{}.sh usr/bin/{}\n", opts.command, opts.command),
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
impl TemplateHooks for Hooks {
|
||||||
/// The file name of the single top-level script (sanitized) pre-fills the
|
/// The file name of the single top-level script (sanitized) pre-fills the
|
||||||
/// package name and command questions.
|
/// package name and command questions.
|
||||||
fn probe(&self, dir: &Path) -> Option<ProbeResult> {
|
fn probe(&self, dir: &Path) -> Option<ProbeResult> {
|
||||||
@@ -58,14 +36,13 @@ impl Template for Shell {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use crate::new::options::{License, SourceDir, TemplateId};
|
use crate::new::options::{License, SourceDir, TemplateId};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn opts() -> NewOptions {
|
fn opts() -> crate::new::options::NewOptions {
|
||||||
NewOptions {
|
crate::new::options::NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Shell,
|
template: TemplateId::SHELL,
|
||||||
source_dir: SourceDir::Skeleton,
|
source_dir: SourceDir::Skeleton,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
@@ -91,27 +68,38 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shell_template_shape() {
|
fn shell_template_shape() {
|
||||||
let o = opts();
|
let o = opts();
|
||||||
let template = super::super::get(TemplateId::Shell).unwrap();
|
let template = super::super::get(TemplateId::SHELL).unwrap();
|
||||||
|
|
||||||
assert_eq!(template.architecture(&o), "all");
|
assert_eq!(template.architecture(&o), "all");
|
||||||
assert!(template.build_depends(&o).is_empty());
|
assert!(template.build_depends(&o).is_empty());
|
||||||
assert!(template.rules_extra(&o).is_empty());
|
assert!(template.rules_extra(&o).is_empty());
|
||||||
|
|
||||||
|
// The skeleton bodies are manifest data now: the executable script
|
||||||
|
// and its skeleton-only install mapping.
|
||||||
let skeleton = template.skeleton(&o);
|
let skeleton = template.skeleton(&o);
|
||||||
assert_eq!(skeleton.len(), 1);
|
assert_eq!(skeleton.len(), 1);
|
||||||
assert_eq!(skeleton[0].path, "mytool.sh");
|
assert_eq!(skeleton[0].path, "mytool.sh");
|
||||||
assert!(skeleton[0].executable);
|
assert!(skeleton[0].executable);
|
||||||
assert!(skeleton[0].contents.starts_with("#!/bin/sh\n"));
|
assert!(skeleton[0].contents.starts_with("#!/bin/sh\n"));
|
||||||
|
assert!(skeleton[0].contents.contains("echo \"Hello from mytool!\""));
|
||||||
|
|
||||||
let debian = template.debian(&o);
|
let debian = template.debian(&o);
|
||||||
assert_eq!(debian.len(), 1);
|
assert_eq!(debian.len(), 1);
|
||||||
assert_eq!(debian[0].path, "debian/install");
|
assert_eq!(debian[0].path, "debian/install");
|
||||||
assert_eq!(debian[0].contents, "mytool.sh usr/bin/mytool\n");
|
assert_eq!(debian[0].contents, "mytool.sh usr/bin/mytool\n");
|
||||||
|
|
||||||
|
// Packaging an existing tree: no install mapping is generated (it
|
||||||
|
// would reference the non-existent skeleton script).
|
||||||
|
let existing = crate::new::options::NewOptions {
|
||||||
|
source_dir: SourceDir::Here,
|
||||||
|
..opts()
|
||||||
|
};
|
||||||
|
assert!(template.debian(&existing).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shell_probe_reads_script_file_name() {
|
fn shell_probe_reads_script_file_name() {
|
||||||
let template = super::super::get(TemplateId::Shell).unwrap();
|
let template = super::super::get(TemplateId::SHELL).unwrap();
|
||||||
|
|
||||||
// The .sh extension is stripped, the stem sanitized into a package
|
// The .sh extension is stripped, the stem sanitized into a package
|
||||||
// name and command.
|
// name and command.
|
||||||
|
|||||||
+1
-1
@@ -126,7 +126,7 @@ mod tests {
|
|||||||
fn opts() -> NewOptions {
|
fn opts() -> NewOptions {
|
||||||
NewOptions {
|
NewOptions {
|
||||||
name: "mytool".into(),
|
name: "mytool".into(),
|
||||||
template: TemplateId::Shell,
|
template: TemplateId::SHELL,
|
||||||
source_dir: SourceDir::Skeleton,
|
source_dir: SourceDir::Skeleton,
|
||||||
upstream_version: "0.1.0".into(),
|
upstream_version: "0.1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
|
|||||||
+2
-2
@@ -18,11 +18,11 @@ use log::{debug, warn};
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/")
|
/// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/")
|
||||||
pub fn ppa_to_base_url(user: &str, name: &str) -> String {
|
pub fn ppa_to_base_url(user: &str, name: &str) -> String {
|
||||||
format!("https://ppa.launchpadcontent.net/{}/{}/ubuntu", user, name)
|
crate::launchpad::ppa_content_url(user, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_launchpad_repo_sync(package: &str) -> Result<Option<String>, String> {
|
fn check_launchpad_repo_sync(package: &str) -> Result<Option<String>, String> {
|
||||||
let url = format!("https://git.launchpad.net/ubuntu/+source/{}", package);
|
let url = crate::launchpad::ubuntu_source_git_url(package);
|
||||||
|
|
||||||
// Use libgit2 to check if the remote repository exists
|
// Use libgit2 to check if the remote repository exists
|
||||||
// This is more reliable than HTTP HEAD requests when CGIt is disabled
|
// This is more reliable than HTTP HEAD requests when CGIt is disabled
|
||||||
|
|||||||
+3
-9
@@ -21,16 +21,14 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use ssh2::{CheckResult, HostKeyType, KnownHostFileKind, KnownHosts, Session};
|
use ssh2::{CheckResult, HostKeyType, KnownHostFileKind, KnownHosts, Session};
|
||||||
|
|
||||||
|
use crate::data::embed_data;
|
||||||
use crate::ui::prompt;
|
use crate::ui::prompt;
|
||||||
|
|
||||||
const HOST_KEYS_YAML: &str = include_str!("../../host_keys.yml");
|
|
||||||
|
|
||||||
/// Pinned SSH host key fingerprints, loaded from the bundled
|
/// Pinned SSH host key fingerprints, loaded from the bundled
|
||||||
/// `host_keys.yml` data file (same pattern as `distro_info.yml`): data
|
/// `host_keys.yml` data file (same pattern as `distro_info.yml`): data
|
||||||
/// rather than code, so trust anchors are updatable without touching the
|
/// rather than code, so trust anchors are updatable without touching the
|
||||||
@@ -42,12 +40,8 @@ struct PinnedHostKeys {
|
|||||||
fingerprints: HashMap<String, Vec<String>>,
|
fingerprints: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
embed_data! {
|
||||||
// The YAML is include_str!'d at compile time and statically valid; if it
|
static ref PINNED_HOST_KEYS: PinnedHostKeys = "../../data/host_keys.yml"
|
||||||
// ever failed to parse it would be a build-time bug that cannot be
|
|
||||||
// recovered from at runtime, so panicking here is acceptable.
|
|
||||||
static ref PINNED_HOST_KEYS: PinnedHostKeys = serde_yaml::from_str(HOST_KEYS_YAML)
|
|
||||||
.expect("built-in host_keys.yml data is statically valid and must parse");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `fingerprint` is one of the pinned (published) fingerprints of
|
/// Whether `fingerprint` is one of the pinned (published) fingerprints of
|
||||||
|
|||||||
+20
-6
@@ -3,7 +3,7 @@
|
|||||||
//! This module provides functionality to read quirks from a YAML file
|
//! This module provides functionality to read quirks from a YAML file
|
||||||
//! and apply them during pull and deb operations.
|
//! and apply them during pull and deb operations.
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use crate::data::embed_data;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -44,9 +44,8 @@ pub struct QuirksConfig {
|
|||||||
pub quirks: HashMap<String, PackageQuirks>,
|
pub quirks: HashMap<String, PackageQuirks>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUIRKS_YAML: &str = include_str!("../quirks.yml");
|
embed_data! {
|
||||||
lazy_static! {
|
static ref QUIRKS_DATA: QuirksConfig = "../data/quirks.yml"
|
||||||
static ref QUIRKS_DATA: QuirksConfig = serde_yaml::from_str(QUIRKS_YAML).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get quirks for a specific package
|
/// Get quirks for a specific package
|
||||||
@@ -75,8 +74,10 @@ pub fn get_package_quirks<'a>(
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Vec<String>` - List of extra dependencies, or empty vector if none
|
/// * `Vec<String>` - List of extra dependencies, or empty vector if none
|
||||||
pub fn get_deb_extra_dependencies(package: &str) -> Vec<String> {
|
pub fn get_deb_extra_dependencies(package: &str) -> Vec<String> {
|
||||||
if let Some(quirks) = &get_package_quirks(&QUIRKS_DATA, package).unwrap().deb {
|
if let Some(quirks) = get_package_quirks(&QUIRKS_DATA, package)
|
||||||
return quirks.extra_dependencies.clone();
|
&& let Some(deb_quirks) = &quirks.deb
|
||||||
|
{
|
||||||
|
return deb_quirks.extra_dependencies.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
@@ -109,3 +110,16 @@ pub fn get_package_directories(package: &str) -> Vec<String> {
|
|||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown_package_has_no_quirks() {
|
||||||
|
// A package absent from quirks.yml (currently every package) has no
|
||||||
|
// extra dependencies nor custom directories, and must not panic
|
||||||
|
assert!(get_deb_extra_dependencies("not-in-quirks").is_empty());
|
||||||
|
assert!(get_package_directories("not-in-quirks").is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user