1
0
Fork 0

Adding upstream version 2.25.15.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
This commit is contained in:
Daniel Baumann 2025-06-21 11:04:07 +02:00
parent 10737b110a
commit b543f2e88d
Signed by: daniel.baumann
GPG key ID: BCC918A2ABD66424
485 changed files with 191459 additions and 0 deletions

184
scripts/Makefile Normal file
View file

@ -0,0 +1,184 @@
include ../Makefile.common
DESTDIR =
define \n
endef
VERSION_FILE = ../version
VERSION = $(shell cat $(VERSION_FILE))
DEB_VENDOR = $(shell dpkg-vendor --query Vendor)
PL_FILES := $(wildcard *.pl)
SH_FILES = $(wildcard *.sh)
SCRIPTS = $(patsubst %.pl,%,$(PL_FILES)) $(patsubst %.sh,%,$(SH_FILES))
PL_CHECKS = $(patsubst %.pl,%.pl_check,$(PL_FILES))
SH_CHECKS = $(patsubst %.sh,%.sh_check,$(SH_FILES))
COMPL_FILES := $(wildcard *.bash_completion)
BC_BUILD_DIR:=bash_completion
COMPLETION = $(patsubst %.bash_completion,$(BC_BUILD_DIR)/%,$(COMPL_FILES))
COMPL_DIR := $(shell pkg-config --variable=completionsdir bash-completion)
PKGNAMES := \
build-rdeps \
dd-list \
deb2apptainer \
deb2docker \
debcheckout \
debsnap \
dget \
getbuildlog \
grep-excuses \
mass-bug \
mk-build-deps \
pts-subscribe \
pts-unsubscribe \
rc-alert \
rmadison \
transition-check \
who-uploads \
whodepends \
wnpp-alert \
wnpp-check \
GEN_MAN1S += \
deb-why-removed.1 \
debbisect.1 \
debftbfs.1 \
debootsnap.1 \
debrebuild.1 \
debrepro.1 \
ltnu.1 \
mk-origtargz.1 \
salsa.1 \
reproducible-check.1 \
uscan.1 \
all: $(SCRIPTS) $(GEN_MAN1S) $(COMPLETION)
scripts: $(SCRIPTS)
$(VERSION_FILE):
$(MAKE) -C .. version
%: %.sh
debchange: debchange.pl $(VERSION_FILE)
sed "s/###VERSION###/$(VERSION)/" $< > $@
chmod --reference=$< $@
ifeq ($(DEB_VENDOR),Ubuntu)
# On Ubuntu always default to targeting the release that it's built on,
# not the current devel release, since its primary use on stable releases
# will be for preparing PPA uploads.
sed -i 's/get_ubuntu_devel_distro()/"$(shell lsb_release -cs)"/' $@
endif
%.tmp: %.sh $(VERSION_FILE)
sed -e "s/###VERSION###/$(VERSION)/" $< > $@
%.tmp: %.pl $(VERSION_FILE)
sed -e "s/###VERSION###/$(VERSION)/" $< > $@
%: %.tmp
cp $< $@
chmod +x $@
%.1: %.pl
podchecker $<
pod2man --utf8 --center=" " --release="Debian Utilities" $< > $@
%.1: %.pod
podchecker $<
pod2man --utf8 --center=" " --release="Debian Utilities" $< > $@
%.1: %.dbk
xsltproc --nonet -o $@ \
/usr/share/sgml/docbook/stylesheet/xsl/nwalsh/manpages/docbook.xsl $<
# Syntax checker
test_sh: $(SH_CHECKS)
%.sh_check: %
bash -n $<
test_pl: $(PL_CHECKS)
%.pl_check: %
perl -I ../lib -c $<; \
devscripts/__init__.py: ../debian/changelog
# Generate devscripts/__init__.py
python3 setup.py build
test_py: devscripts/__init__.py
$(foreach python,$(shell py3versions -r ../debian/control),$(python) -m unittest discover devscripts$(\n))
debbisect.1: debbisect
help2man \
--name="bisect snapshot.debian.org" \
--version-string=$(VERSION) \
--no-info \
--no-discard-stderr \
./$< >$@
debftbfs.1: debftbfs
help2man \
--name="list packages that have FTBFS bugs filed" \
--version-string=$(VERSION) \
--no-info \
--no-discard-stderr \
./$< >$@
debootsnap.1: debootsnap
help2man \
--name="create debian chroot using snapshot.debian.org" \
--version-string=$(VERSION) \
--no-info \
--no-discard-stderr \
./$< >$@
debrebuild.1: debrebuild
help2man \
--name="use a buildinfo file and snapshot.d.o to recreate binary packages" \
--version-string=$(VERSION) \
--no-info \
--no-discard-stderr \
./$< >$@
reproducible-check.1: reproducible-check
help2man \
--name="Reports on the reproducible status of installed packages" \
--no-info \
--no-discard-stderr \
./$< >$@
$(BC_BUILD_DIR):
mkdir $(BC_BUILD_DIR)
$(COMPLETION): $(BC_BUILD_DIR)/% : %.bash_completion $(BC_BUILD_DIR)
cp $< $@
clean:
rm -f devscripts/__init__.py
find -name '*.pyc' -delete
find -name __pycache__ -delete
rm -rf devscripts.egg-info $(BC_BUILD_DIR) .pylint.d
rm -f $(SCRIPTS) $(patsubst %,%.tmp,$(SCRIPTS)) \
$(GEN_MAN1S) $(SCRIPT_LIBS)
rm -rf build
test: test_pl test_sh test_py
install: all
python3 setup.py install --root="$(DESTDIR)" --no-compile --install-layout=deb
cp $(SCRIPTS) $(DESTDIR)$(BINDIR)
ln -sf edit-patch $(DESTDIR)$(BINDIR)/add-patch
install -d $(DESTDIR)$(COMPL_DIR)
cp $(BC_BUILD_DIR)/* $(DESTDIR)$(COMPL_DIR)/
for i in $(PKGNAMES); do \
ln -sf pkgnames $(DESTDIR)$(COMPL_DIR)/$$i; \
done
ln -sf debchange $(DESTDIR)$(COMPL_DIR)/dch
ln -sf debi $(DESTDIR)$(COMPL_DIR)/debc
# Special treatment for run_bisect
install -d $(DESTDIR)$(DATA_DIR)/scripts
mv $(DESTDIR)$(BINDIR)/run_bisect $(DESTDIR)$(DATA_DIR)/scripts
mv $(DESTDIR)$(BINDIR)/run_bisect_qemu $(DESTDIR)$(DATA_DIR)/scripts
.PHONY: test test_pl test_sh test_py all install clean scripts

120
scripts/annotate-output.1 Normal file
View file

@ -0,0 +1,120 @@
.TH ANNOTATE-OUTPUT 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
annotate-output \- annotate program output with time and stream
.SH SYNOPSIS
\fBannotate\-output\fR [\fIoptions\fR ...] [--] \fIprogram\fR [\fIargs\fR ...]
.SH DESCRIPTION
\fBannotate\-output\fR executes \fIprogram\fR with \fIargs\fR as arguments
and prepends printed lines with a format string followed by an indicator
for the stream on which the line was printed followed by a colon and a
single space.
.br
The stream indicators are \fBI\fR for information from
\fBannotate\-output\fR as well as \fBO\fR for STDOUT and \fBE\fR for STDERR
from \fIprogram\fR.
.SH OPTIONS
.TP
\fB+FORMAT\fR
A format string that may use the conversion specifiers from the
\fBdate\fR(1)-utility. The printed string is separated from the following
stream indicator by a single space. May be overridden by later options that
specify the format string.
.br
Defaults to "%H:%M:%S".
.TP
\fB--raw-date-format\fR \fIFORMAT\fR
A format string that may use the conversion specifiers from the
\fBdate\fR(1)-utility. There is no separator between the printed string and
the following stream indicator. May be overridden by later options that
specify the format string.
.TP
\fB--\fR
Ends option parsing (unless it is itself an argument to an option).
.TP
\fB\-h\fR, \fB\-\-help\fR
Display a help message.
.SH EXIT STATUS
If \fIprogram\fR is invoked, the exit status of \fBannotate\-output\fR
shall be the exit status of \fIprogram\fR; otherwise,
\fBannotate\-output\fR shall exit with one of the following values:
.TP
0
\fB\-h\fR or \fB\-\-help\fR was used.
.TP
125
An error occurred in \fBannotate\-output\fR.
.TP
126
\fIprogram\fR was found but could not be invoked.
.TP
127
\fIprogram\fR could not be found or was not specified.
.SH EXAMPLE
.nf
$ annotate-output make
21:41:21 I: Started make
21:41:21 O: gcc \-Wall program.c
21:43:18 E: program.c: Couldn't compile, and took me ages to find out
21:43:19 E: collect2: ld returned 1 exit status
21:43:19 E: make: *** [all] Error 1
21:43:19 I: Finished with exitcode 2
.fi
.SH CAVEATS AND BUGS
Since STDOUT and STDERR are processed in parallel, it can happen that
some lines received on STDOUT will show up before later-printed STDERR
lines (and vice-versa).
.br
This is unfortunately very hard to fix with the current annotation
strategy. A fix would involve switching to PTRACE'ing the process.
Giving nice a (much) higher priority over \fIprogram\fR could
however cause this behaviour to show up less frequently.
\fBannotate\-output\fR expects \fIprogram\fR to output (text) lines (as
specified by POSIX) to STDOUT and STDERR.
.br
In particular, it leads to undefined behaviour when lines are printed that
contain NUL bytes. It further may lead to undefined behaviour when lines
are printed that contain bytes that do not form valid characters in the
current locale.
When an interactive \fIprogram\fR asks for input, the question might not be
shown until after you have answered it. This will give the impression that
\fIprogram\fR has hung, while it has not.
\fBannotate\-output\fR is implemented as a script in the Shell Command
Language. Shells typically set various (shell) variables when started and
may set the `export` attribute on some of them. They further initialise
(shell) variables from their own environment (as set by the caller of the
shell respectively the caller of \fBannotate\-output\fR) and set the
`export` attribute on them.
.br
It follows from this, that when the caller of \fBannotate\-output\fR wants
to set the environment (variables) of \fIprogram\fR, they may get
overridden or additional ones may get added by the shell.
.br
Further, environment variables are in principle allowed to have names (for
example `.`) that are not valid shell variable names. POSIX does not
specify whether or not such environment variables are exported to programs
invoked from the shell. No assumptions can thus be made on whether such
environment variables will be exported correctly or at all to \fIprogram\fR.
.SH "SEE ALSO"
\fBdate\fR(1)
.SH SUPPORT
\fBannotate\-output\fR is community-supported (meaning: you'll need to fix
it yourself). Patches are however appreciated, as is any feedback
(positive or negative).
.SH AUTHOR
This manual page was written by Jeroen van Wolffelaar <jeroen@wolffelaar.nl>
and can be redistributed under the terms of the GPL version 2.
The \fBannotate-output\fR script itself was re-written by Johannes Schauer
Marin Rodrigues <josch@debian.org> and can be redistributed under the terms
of the Expat license.

140
scripts/annotate-output.sh Executable file
View file

@ -0,0 +1,140 @@
#!/bin/sh
# Copyright 2019-2023 Johannes Schauer Marin Rodrigues <josch@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
set -eu
# TODO: Switch from using `/usr/bin/printf` to the (likely built-in) `printf`
# once POSIX has standardised `%q` for that
# (see https://austingroupbugs.net/view.php?id=1771) and `dash`
# implemented it.
define_get_prefix() {
eval " get_prefix() {
/usr/bin/printf '%q' $(/usr/bin/printf '%q' "$1")
}"
}
define_handler_with_date_conversion_specifiers() {
eval " handler() {
while IFS= read -r line; do
printf '%s%s: %s\\n' \"\$(date $(/usr/bin/printf '%q' "$1") )\" \"\$1\" \"\$line\"
done
if [ -n \"\$line\" ]; then
printf '%s%s: %s' \"\$(date $(/usr/bin/printf '%q' "$1") )\" \"\$1\" \"\$line\"
fi
}"
define_get_prefix "${1#+}"
}
define_handler_with_plain_prefix() {
eval " handler() {
while IFS= read -r line; do
printf '%s%s: %s\\n' $(/usr/bin/printf '%q' "$1") \"\$1\" \"\$line\"
done
if [ -n \"\$line\" ]; then
printf '%s%s: %s' $(/usr/bin/printf '%q' "$1") \"\$1\" \"\$line\"
fi
}"
define_get_prefix "$1"
}
usage() {
printf \
'Usage: %s [OPTIONS ...] [--] PROGRAM [ARGS ...]
Executes PROGRAM with ARGS as arguments and prepends printed lines with a format
string, a stream indicator and `: `.
Options:
+FORMAT
A format string that may use the conversion specifiers from the `date`(1)-
utility.
The printed string is separated from the following stream indicator by a
single space.
Defaults to `%%H:%%M:%%S`.
--raw-date-format FORMAT
A format string that may use the conversion specifiers from the `date`(1)-
utility.
The printed string is not separated from the following stream indicator.
-h
--help
Display this help message.
' "${0##*/}"
}
define_handler_with_date_conversion_specifiers '+%H:%M:%S '
while [ -n "${1-}" ]; do
case "$1" in
+*%*)
define_handler_with_date_conversion_specifiers "$1 "
shift
;;
+*)
define_handler_with_plain_prefix "${1#+} "
shift
;;
--raw-date-format)
if [ "$#" -lt 2 ]; then
printf '%s: The `--raw-date-format`-option requires an argument.\n' "${0##*/}" >&2
exit 125
fi
case "$2" in
*%*) define_handler_with_date_conversion_specifiers "+$2";;
*) define_handler_with_plain_prefix "${2#+}";;
esac
shift 2
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
*)
break
;;
esac
done
if [ "$#" -lt 1 ]; then
printf '%s: No program to be executed was specified.\n' "${0##*/}" >&2
exit 127
fi
printf 'I: annotate-output %s\n' '###VERSION###'
printf 'I: prefix='
get_prefix
printf '\n'
{ printf 'Started'; /usr/bin/printf ' %q' "$@"; printf '\n'; } | handler I
# The following block redirects FD 2 (STDERR) to FD 1 (STDOUT) which is then
# processed by the STDERR handler. It redirects FD 1 (STDOUT) to FD 4 such
# that it can later be moved to FD 1 (STDOUT) and handled by the STDOUT handler.
# The exit status of the program gets written to FD 2 (STDERR) which is then
# captured to produce the correct exit status as the last step of the pipe.
# Both the STDOUT and STDERR handler output to FD 3 such that after exiting
# with the correct exit code, FD 3 can be redirected to FD 1 (STDOUT).
{
{
{
{
{
"$@" 2>&1 1>&4 3>&- 4>&-; printf "$?\n" >&2;
} | handler E >&3;
} 4>&1 | handler O >&3;
} 2>&1;
} | { IFS= read -r xs; exit "$xs"; };
} 3>&1 && { printf 'Finished with exitcode 0\n' | handler I; exit 0; } \
|| { err="$?"; printf "Finished with exitcode $err\n" | handler I; exit "$err"; }

63
scripts/archpath.1 Normal file
View file

@ -0,0 +1,63 @@
.TH ARCHPATH 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
archpath \- output arch (tla/Bazaar) archive names, with support for branches
.SH SYNOPSIS
.B archpath
.br
.B archpath
.I branch
.br
.B archpath
.IR branch \fB--\fI version
.SH DESCRIPTION
.B archpath
is intended to be run in an arch (tla or Bazaar) working copy.
.PP
In its simplest usage,
.B archpath
with no parameters outputs the package name
(archive/category--branch--version) associated with the working copy.
.PP
If a parameter is given, it may either be a branch--version, in which case
.B archpath
will output a corresponding package name in the current archive and
category, or a plain branch name (without \(oq--\(dq), in which case
.B archpath
will output a corresponding package name in the current archive and
category and with the same version as the current working copy.
.PP
This is useful for branching.
For example, if you're using Bazaar and you want to create a branch for a
new feature, you might use a command like this:
.PP
.RS
.nf
.ft CW
baz branch $(archpath) $(archpath new-feature)
.ft R
.fi
.RE
.PP
Or if you want to tag your current code onto a \(oqreleases\(cq branch as
version 1.0, you might use a command like this:
.PP
.RS
.nf
.ft CW
baz branch $(archpath) $(archpath releases--1.0)
.ft R
.fi
.RE
.PP
That's much easier than using \(oqbaz tree-version\(cq to look up the
package name and manually modifying the result.
.SH AUTHOR
.B archpath
was written by
.na
Colin Watson <cjwatson@debian.org>.
.ad
Like
.BR archpath ,
this manual page is released under the GNU General Public License,
version 2 or later.

46
scripts/archpath.sh Executable file
View file

@ -0,0 +1,46 @@
#!/bin/bash
# Output arch (tla/Bazaar) archive names, with support for branches
# Copyright (C) 2005 Colin Watson <cjwatson@debian.org>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -e
# Which arch implementation should we use?
if type baz > /dev/null 2>&1; then
PROGRAM=baz
else
PROGRAM=tla
fi
WANTED="$1"
ME="$($PROGRAM tree-version)"
if [ "$WANTED" ]; then
ARCHIVE="$($PROGRAM parse-package-name --arch "$ME")"
CATEGORY="$($PROGRAM parse-package-name --category "$ME")"
case $WANTED in
*--*)
echo "$ARCHIVE/$CATEGORY--$WANTED"
;;
*)
VERSION="$($PROGRAM parse-package-name --vsn "$ME")"
echo "$ARCHIVE/$CATEGORY--$WANTED--$VERSION"
;;
esac
else
echo "$ME"
fi

319
scripts/bts.bash_completion Normal file
View file

@ -0,0 +1,319 @@
# /usr/share/bash-completion/completions/bts
# Bash command completion for bts(1).
# Documentation: bash(1), section “Programmable Completion”.
# Copyright © 2015, Nicholas Bamber <nicholas@periapt.co.uk>
_get_version_from_package()
{
local _pkg=$1
[[ -n $_pkg ]] || return
apt-cache madison $_pkg 2> /dev/null | cut -d'|' -f2 | sort | uniq | paste -s -d' '
}
# This works really well unless someone sets up nasty firewall rules like:
# sudo iptables -A OUTPUT -d 206.12.19.140 -j DROP
# sudo iptables -A OUTPUT -d 140.211.166.26 -j DROP
# These block access to the Debian bugs SOAP interface.
# Hence we need a timeout.
# Of course if the SOAP interface is blocked then so is the caching interface.
# So really this would only affect someone who only accidentally hit the TAB key.
_get_version_from_bug()
{
local -i _bug=$1
_get_version_from_package $( bts --soap-timeout=2 status $_bug fields:package 2> /dev/null | cut -f2 )
}
_suggest_packages()
{
apt-cache --no-generate pkgnames "$1" 2> /dev/null
}
_suggest_bugs()
{
bts --offline listcachedbugs "$1" 2> /dev/null
}
_bts()
{
local cur prev words cword
_init_completion -n = || return
# Note:
# The long lists of subcommands are not the same and not necessarily to be kept in sync.
# The first is used to suggest commands after a '.' or ','.
# The second is to hook in special handling (which may be as little as admitting we
# we can't handle it further) or the default special handling (list of bug ids).
# This also includes "by" and "with" which are not even subcommands.
# The third is similar to the first - what to suggest after the bts command (and options).
# but this includes the "help" and "version" commands.
# A sequence of bts commands can be on one command line separated by "." or ",".
if [[ $prev == @(.|,) ]]; then
COMPREPLY=( $( compgen -W 'show bugs unmerge select status clone done reopen archive unarchive retitle summary submitter reassign found notfound fixed notfixed block unblock merge forcemerge tags affects user usertags claim unclaim severity forwarded notforwarded package limit owner noowner subscribe unsubscribe reportspam spamreport' -- "$cur" ) )
return 0
fi
# Identify the last command in the command line.
local special punctuation i
for (( i=${#words[@]}-1; i > 0; i-- )); do
if [[ ${words[i]} == @(show|bugs|select|limit|unmerge|status|clone|done|reopen|archive|unarchive|retitle|summary|submitter|reassign|found|notfound|fixed|notfixed|block|unblock|merge|forcemerge|tags|affects|user|usertags|claim|unclaim|severity|forwarded|notforwarded|package|owner|noowner|subscribe|unsubscribe|reportspam|spamreport|cache|cleancache|by|with) ]]; then
special=${words[i]}
break
fi
if [[ ${words[i]} == @(+|-|=) ]]; then
punctuation=${words[i]}
fi
done
if [[ -n $special ]]; then
# The command separator must be surrounded by white space.
if [[ "$cur" == @(,|.) ]]; then
COMPREPLY=( $cur )
return 0
fi
case $special in
show|bugs)
# bugs/show supports a few limited options
# but as args we accept bug ids, package names and release-critical
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W '-o --offline --online -m --mbox \
--no-cache --cache' -- "$cur" ) )
elif [[ "$cur" == release-critical/* ]]; then
local _pkg=${cur#release-critical/}
COMPREPLY=( $( _suggest_packages "$_pkg" | sed -e's!^!release-critical/!' ) )
else
COMPREPLY=( $( compgen -W 'release-critical RC' -- "$cur" ) \
$( _suggest_bugs "$cur" ) \
$( _suggest_packages "$cur" ) )
fi
return 0
;;
status)
# we accept "verbose" and bug ids
COMPREPLY=( $( compgen -W 'verbose' -- "$cur" ) \
$( _suggest_bugs "$cur" ) )
return 0
;;
clone)
# we accept 1 bug id and then generate new clone ids
if [[ "$prev" == +([0-9]) ]]; then
COMPREPLY=( $( compgen -W '-1' -- "$cur" ) )
elif [[ "$prev" == -+([0-9]) ]]; then
local -i j
(( j=$prev-1 ))
COMPREPLY=( $( compgen -W $j -- "$cur" ) )
else
COMPREPLY=( $( _suggest_bugs "$cur" ) )
fi
return 0
;;
done|found|notfound|fixed|notfixed)
# Try to guess the version
if [[ "$prev" == +([0-9]) ]]; then
local _versions=$( _get_version_from_bug $prev )
if [[ -n $_versions ]]; then
COMPREPLY=( $( compgen -W $_versions -- "$cur" ) )
else
COMPREPLY=( )
fi
else
COMPREPLY=( $( _suggest_bugs "$cur" ) )
fi
return 0
;;
reopen|claim|unclaim|owner|subscribe|unsubscribe)
if [[ "$prev" == +([0-9]) && -n $DEBEMAIL ]]; then
COMPREPLY=( $( compgen -W $DEBEMAIL -- "$cur" ) )
else
COMPREPLY=( $( _suggest_bugs "$cur" ) )
fi
return 0
;;
reassign)
# Must have at least one bug id.
# Once we have a package name, all that remains is an optional version.
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) )
elif [[ "$prev" == +([0-9]) ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) \
$( _suggest_packages "$cur" ) )
else
local _versions=$( _get_version_from_package $prev )
COMPREPLY=( $( compgen -W $_versions -- "$cur" ) )
fi
return 0
;;
block|unblock)
# Must have at least one bug id.
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) )
elif [[ "$prev" == +([0-9]) ]]; then
COMPREPLY=( $( compgen -W 'by with' -- "$cur" ) )
else
COMPREPLY=( )
fi
return 0
;;
unmerge|forwarded|notforwarded|noowner)
# Must have at most one bug id.
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) )
else
COMPREPLY=( )
fi
return 0
;;
tags)
# Must have one bug id.
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) )
elif [[ -n $punctuation ]]; then
# The official list is mirrored
# https://www.debian.org/Bugs/server-control#tag
# in the variable @gTags; we copy it verbatim here.
COMPREPLY=( $( compgen -W 'patch wontfix moreinfo unreproducible fixed potato woody sid help security upstream pending sarge sarge-ignore experimental d-i confirmed ipv6 lfs fixed-in-experimental fixed-upstream l10n newcomer a11y ftbfs etch etch-ignore lenny lenny-ignore squeeze squeeze-ignore wheezy wheezy-ignore jessie jessie-ignore stretch stretch-ignore buster buster-ignore bullseye bullseye-ignore' -- "$cur" ) )
else
COMPREPLY=()
COMPREPLY[0]='= '
COMPREPLY[1]='+ '
COMPREPLY[2]='- '
fi
return 0
;;
affects)
# Must have one bug id.
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) )
elif [[ -n $punctuation ]]; then
COMPREPLY=( $( _suggest_packages "$cur" ) )
else
COMPREPLY=()
COMPREPLY[0]='= '
COMPREPLY[1]='+ '
COMPREPLY[2]='- '
fi
return 0
;;
user)
if [[ "$prev" == $special && -n $DEBEMAIL ]]; then
COMPREPLY=( $( compgen -W $DEBEMAIL -- "$cur" ) )
else
COMPREPLY=( )
fi
return 0
;;
usertags)
# Must have one bug id.
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) )
elif [[ -z $punctuation ]]; then
COMPREPLY=()
COMPREPLY[0]='= '
COMPREPLY[1]='+ '
COMPREPLY[2]='- '
else
COMPREPLY=()
fi
return 0
;;
severity)
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( _suggest_bugs "$cur" ) )
elif [[ "$prev" == +([0-9]) ]]; then
COMPREPLY=( $( compgen -W 'wishlist minor normal important serious \
grave critical' -- "$cur" ) )
else
COMPREPLY=()
fi
return 0
;;
select|limit)
# can't handle ":". Give up for now.
COMPREPLY=( )
return 0
;;
package)
COMPREPLY=( $( _suggest_packages "$cur" ) )
return 0
;;
cache)
# cache supports a few limited options
# but as args we accept bug ids, package names and release-critical
if [[ "$prev" == --cache-mode ]]; then
COMPREPLY=( $( compgen -W 'min mbox full' -- "$cur" ) )
elif [[ "$cur" == release-critical/* ]]; then
local _pkg=${cur#release-critical/}
COMPREPLY=( $( _suggest_packages "$_pkg" | sed -e's!^!release-critical/!' ) )
elif [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W '--cache-mode --force-refresh -f \
--include-resolved -q --quiet' -- "$cur" ) )
else
COMPREPLY=( $( compgen -W 'release-critical RC' -- "$cur" ) \
$( _suggest_packages "$cur" ) )
fi
return 0
;;
cleancache)
if [[ "$prev" == $special ]]; then
COMPREPLY=( $( compgen -W 'ALL' -- "$cur" ) \
$( _suggest_bugs "$cur" ) \
$( _suggest_packages "$cur" ) )
else
COMPREPLY=( )
fi
return 0
;;
*)
COMPREPLY=( $( _suggest_bugs "$cur" ) )
return 0
;;
esac
fi
case $prev in
--cache-mode)
COMPREPLY=( $( compgen -W 'min mbox full' -- "$cur" ) )
return 0
;;
--cache-delay)
COMPREPLY=( $( compgen -W '5 60 120 240 600' -- "$cur" ) )
return 0
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W '-o --offline --online -n --no-action --cache --no-cache --cache-mode --cache-delay --mbox --no-use-default-cc --mutt --no-mutt -f --force-refresh --no-force-refresh --only-new --include-resolved --no-include-resolved --no-ack --ack -i --interactive --force-interactivei --no-interactive -q --quiet' -- "$cur" ) )
else
COMPREPLY=( $( compgen -W 'show bugs unmerge select status clone done reopen archive unarchive retitle summary submitter reassign found notfound fixed notfixed block unblock merge forcemerge tags affects user usertags claim unclaim severity forwarded notforwarded package limit owner noowner subscribe unsubscribe reportspam spamreport cache cleancache version help' -- "$cur" ) )
fi
# !!! not handled !!!
# --mailreader=READER
# --cc-addr=CC_EMAIL_ADDRESS
# --use-default-cc
# --sendmail=SENDMAILCMD
# --smtp-host=SMTPHOST
# --smtp-username=USERNAME
# --smtp-password=PASSWORD
# --smtp-helo=HELO
# --bts-server
# --no-conf, --noconf
#
# anything with colons for now
# for similar reasons having issues with tags XXXX =
# no special handling for select
return 0
} &&
complete -F _bts bts
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

4352
scripts/bts.pl Executable file

File diff suppressed because it is too large Load diff

747
scripts/build-rdeps.pl Executable file
View file

@ -0,0 +1,747 @@
#!/usr/bin/perl
# -*- tab-width: 4; indent-tabs-mode: t; cperl-indent-level: 4 -*-
# vim: set ai shiftwidth=4 tabstop=4 expandtab:
# Copyright (C) Patrick Schoenfeld
# 2015 Johannes Schauer Marin Rodrigues <josch@debian.org>
# 2017 James McCoy <jamessan@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
build-rdeps - find packages that depend on a specific package to build (reverse build depends)
=head1 SYNOPSIS
B<build-rdeps> I<package> [I<package> ...]
=head1 DESCRIPTION
B<build-rdeps> searches for all source packages that build-depend on any of the specified binary packages.
The default behaviour is to just `grep` for the given dependencies in the
Build-Depends field of apt's Sources files.
If the package dose-extra >= 4.0 is installed, then a more complete reverse
build dependency computation is carried out. In particular, with B<dose-extra>
installed, B<build-rdeps> will find transitive reverse dependencies, respect
architecture and build profile restrictions, take Provides relationships,
Conflicts, Pre-Depends, Build-Depends-Arch and versioned dependencies into
account and correctly resolve multiarch relationships for crossbuild reverse
dependency resolution. This tends to be a slow process due to the complexity
of the package interdependencies. If you need to find the reverse dependencies
of more than one binary package, consider supplying all binary packages as
additional arguments instead of calling B<build-rdeps> multiple times.
=head1 OPTIONS
=over 4
=item B<-u>, B<--update>
Run apt-get update before searching for build-depends.
=item B<-s>, B<--sudo>
Use sudo when running apt-get update. Has no effect if -u is omitted.
=item B<--distribution>
Select another distribution, which is searched for build-depends.
=item B<--only-main>
Ignore contrib, non-free and non-free-firmware.
=item B<--only-devel>
Consider only development distributions (e.g. unstable, sid).
=item B<--exclude-component>
Ignore the given component (e.g. main, contrib, non-free, non-free-firmware).
=item B<--origin>
Restrict the search to only the specified origin (such as "Debian").
=item B<-m>, B<--print-maintainer>
Print the value of the maintainer field for each package.
=item B<--host-arch>
Explicitly set the host architecture. The default is the value of
`dpkg-architecture -qDEB_HOST_ARCH`. This option only works if dose-extra >=
4.0 is installed.
=item B<--build-arch>
Explicitly set the build architecture. The default is the value of
`dpkg-architecture -qDEB_BUILD_ARCH`. This option only works if dose-extra >=
4.0 is installed.
=item B<--no-arch-all>, B<--no-arch-any>
Ignore Build-Depends-Indep or Build-Depends-Arch while looking for reverse
dependencies.
=item B<--no-ftbfs>
Do not output source packages which have open FTBFS bugs in the selected
distribution. This functionality uses the B<debftbfs> utility.
=item B<--old>
Force the old simple behaviour without dose-ceve support even if dose-extra >=
4.0 is installed. (This tends to be faster.)
Notice, that the old behaviour only finds direct dependencies, ignores virtual
dependencies, does not find transitive dependencies and does not take version
relationships, architecture restrictions, build profiles or multiarch
relationships into account.
=item B<-q>, B<--quiet>
Don't print meta information (header, counter). Making it easier to use in
scripts.
=item B<-d>, B<--debug>
Run the debug mode
=item B<--help>
Show the usage information.
=item B<--version>
Show the version information.
=back
=head1 REQUIREMENTS
The tool requires apt Sources files to be around for the checked components.
In the default case this means that in /var/lib/apt/lists files need to be
around for main, contrib, non-free and non-free-firmware.
In practice this means one needs to add one deb-src line for each component,
e.g.
deb-src http://<mirror>/debian <dist> main contrib non-free non-free-firmware
and run apt-get update afterwards or use the update option of this tool.
=cut
use warnings;
use strict;
use File::Basename;
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use File::Temp qw(tempfile tempdir);
use Dpkg::Control;
use Dpkg::Vendor qw(get_current_vendor);
use Dpkg::IPC;
use Dpkg::Path qw(find_command);
use English;
my $progname = basename($0);
my $version = '1.0';
my $use_ceve = 0;
my $ceve_compatible;
my $opt_debug;
my $opt_update;
my $opt_sudo;
my $opt_maintainer;
my $opt_mainonly;
my $opt_develonly = 0;
my $opt_distribution;
my $opt_origin = get_current_vendor();
my @opt_exclude_components;
my $opt_buildarch;
my $opt_hostarch;
my $opt_without_ceve;
my $opt_quiet;
my $opt_noarchall;
my $opt_noarchany;
my $opt_noftbfs;
sub version {
print <<"EOT";
This is $progname $version, from the Debian devscripts package, v. ###VERSION###
This code is copyright by Patrick Schoenfeld, all rights reserved.
It comes with ABSOLUTELY NO WARRANTY. You are free to redistribute this code
under the terms of the GNU General Public License, version 2 or later.
EOT
exit(0);
}
sub usage {
print <<"EOT";
usage: $progname packagename
$progname --help
$progname --version
Searches for all packages that build-depend on the specified package.
Options:
-u, --update Run apt-get update before searching for build-depends.
(needs root privileges)
-s, --sudo Use sudo when running apt-get update
(has no effect when -u is omitted)
-q, --quiet Don't print meta information
-d, --debug Enable the debug mode
-m, --print-maintainer Print the maintainer information (experimental)
--distribution distribution Select a distribution to search for build-depends
--origin origin Select an origin to search for build-depends
(Default: Debian)
--only-main Ignore contrib, non-free and non-free-firmware
--only-devel Consider only development distributions
--exclude-component COMPONENT Ignore the specified component (can be given multiple times)
--host-arch Set the host architecture (requires dose-extra >= 4.0)
--build-arch Set the build architecture (requires dose-extra >= 4.0)
--no-arch-all Ignore Build-Depends-Indep
--no-arch-any Ignore Build-Depends-Arch
--no-ftbfs Ignore source packages with open FTBFS bugs (uses debftbfs)
--old Use the old simple reverse dependency resolution
EOT
version;
}
sub debug {
my $msg = shift;
print STDERR "DEBUG: $msg\n" if $opt_debug;
}
sub test_ceve {
return $ceve_compatible if defined $ceve_compatible;
# test if the debsrc input and output format is supported by the installed
# ceve version
system('dose-ceve -T debsrc debsrc:///dev/null > /dev/null 2>&1');
if ($? == -1) {
debug "dose-ceve cannot be executed: $!";
$ceve_compatible = 0;
} elsif ($? == 0) {
$ceve_compatible = 1;
} else {
debug 'dose-ceve is too old';
$ceve_compatible = 0;
}
return $ceve_compatible;
}
sub is_devel_release {
my $ctrl = shift;
if ($opt_origin eq 'Debian') {
return $ctrl->{Suite} eq 'unstable' || $ctrl->{Codename} eq 'sid';
} else {
return $ctrl->{Suite} eq 'devel';
}
}
sub indextargets {
my @cmd = ('apt-get', 'indextargets', 'DefaultEnabled: yes');
if (!$use_ceve) {
# ceve needs both Packages and Sources
push(@cmd, 'Created-By: Sources');
}
if ($opt_origin) {
push(@cmd, "Origin: $opt_origin");
}
if ($opt_mainonly) {
push(@cmd, 'Component: main');
}
debug 'Running ' . join(' ', map { "'$_'" } @cmd);
return @cmd;
}
# Gather information about the available package/source lists.
#
# Returns a hash reference following this structure:
#
# <site> => {
# <suite> => {
# <component> => {
# sources => $src_fname,
# <arch1> => $arch1_fname,
# ...,
# },
# },
# ...,
sub collect_files {
my %info = ();
open(my $targets, '-|', indextargets());
until (eof $targets) {
my $ctrl = Dpkg::Control->new(type => CTRL_UNKNOWN);
if (!$ctrl->parse($targets, 'apt-get indextargets')) {
next;
} else {
debug "index targets stanza parsed";
print STDERR $ctrl if ($opt_debug);
}
# Only need Sources/Packages stanzas
if ( $ctrl->{'Created-By'} ne 'Packages'
&& $ctrl->{'Created-By'} ne 'Sources') {
debug qq("Created-By: $ctrl->{'Created-By'}" )
. qq(not Packages/Sources\n);
next;
}
# In expected components
if ( !$opt_mainonly
&& exists $ctrl->{Component}
&& @opt_exclude_components) {
my $invalid_component = '(?:'
. join('|', map { "\Q$_\E" } @opt_exclude_components) . ')';
if ($ctrl->{Component} =~ m/$invalid_component/) {
debug qq("Component: $ctrl->{Component}" )
. qq(not $invalid_component\n);
next;
}
}
# And the provided distribution
if ( !exists $ctrl->{Suite}
|| !exists $ctrl->{Codename}) {
debug "no Suite or no Codename\n";
next;
} elsif ($opt_distribution) {
if ( $ctrl->{Suite} !~ m/\Q$opt_distribution\E/
&& $ctrl->{Codename} !~ m/\Q$opt_distribution\E/) {
debug qq("Suite: $ctrl->{Suite}" and )
. qq("Codename: $ctrl->{Codename}" )
. qq(not $opt_distribution\n);
next;
}
} elsif ($opt_develonly && !is_devel_release($ctrl)) {
debug qq("Suite: $ctrl->{Suite}" and )
. qq("Codename: $ctrl->{Codename}" )
. qq(not devel release\n);
next;
}
$info{ $ctrl->{Site} }{ $ctrl->{Suite} }{ $ctrl->{Component} } ||= {};
my $ref
= $info{ $ctrl->{Site} }{ $ctrl->{Suite} }{ $ctrl->{Component} };
if ($ctrl->{'Created-By'} eq 'Sources') {
$ref->{sources} = $ctrl->{Filename};
debug "Added source file: $ctrl->{Filename}";
} else {
$ref->{ $ctrl->{Architecture} } = $ctrl->{Filename};
debug "Added $ctrl->{Architecture} packages "
. "file: $ctrl->{Filename}";
}
print STDERR "\n"
if $opt_debug;
}
close($targets);
return \%info;
}
# File::Temp has an END block which cleans up the temporary directory
# we created with CLEANUP=>1 but we have to explicitly die() or otherwise
# the interpreter will exit on HUP, INT, PIPE and TERM instead of calling
# the END block
use sigtrap qw(die normal-signals);
sub findreversebuilddeps {
my ($info, $comp, @packages) = @_;
my $count = 0;
my %ftbfs = ();
# if desired, use debftbfs to prevent reverse dependencies from being
# printed which are currently known to be unbuildable
if ($opt_noftbfs) {
my $debftbfs_exe = "debftbfs";
# if build-rdeps is run from a git clone, also use debftbfs from here
if ($PROGRAM_NAME eq "scripts/build-rdeps.pl"
&& -x "scripts/debftbfs") {
$debftbfs_exe = "scripts/debftbfs";
}
my $debftbfs;
my $debftbfs_pid = spawn(
exec => [
$debftbfs_exe,
(
$opt_distribution
? ('--distribution', $opt_distribution)
: ()
),
'--source',
'udd.d.o'
],
to_pipe => \$debftbfs
);
while (my $line = <$debftbfs>) {
my $src = (split /\s+/, $line, 2)[0];
$ftbfs{$src} = 1;
}
close($debftbfs);
wait_child($debftbfs_pid, nocheck => 1, cmdline => "debftbfs");
}
my $source_file = $info->{$comp}->{sources};
if ($use_ceve) {
die "build arch undefined" if !defined $opt_buildarch;
die "host arch undefined" if !defined $opt_hostarch;
my $buildarch_file = $info->{$comp}->{$opt_buildarch};
my $hostarch_file = $info->{$comp}->{$opt_hostarch};
my $tmpdir = tempdir('build-rdepsXXXXXX', TMPDIR => 1, CLEANUP => 1);
(undef, my $tmp_buildarch_file)
= tempfile('Packages_build.XXXXXX', OPEN => 0, DIR => $tmpdir);
(undef, my $tmp_hostarch_file)
= tempfile('Packages_host.XXXXXX', OPEN => 0, DIR => $tmpdir);
(undef, my $tmp_source_file)
= tempfile('Sources.XXXXXX', OPEN => 0, DIR => $tmpdir);
spawn(
exec => ['/usr/lib/apt/apt-helper', 'cat-file', $buildarch_file],
to_file => $tmp_buildarch_file,
wait_child => 1
);
spawn(
exec => ['/usr/lib/apt/apt-helper', 'cat-file', $source_file],
to_file => $tmp_source_file,
wait_child => 1
);
my @ceve_cmd = (
'dose-ceve', "--deb-native-arch=$opt_buildarch",
'-T', 'debsrc',
'-r', (join ',', @packages),
'-G', 'pkg',
"deb://$tmp_buildarch_file", "debsrc://$tmp_source_file"
);
if ($comp ne "main") {
# if this is not "main", also add "main" to the mix, to resolve
# dependencies correctly
(undef, my $tmp_buildarch_file_main) = tempfile(
'Packages_build_main.XXXXXX',
OPEN => 0,
DIR => $tmpdir
);
spawn(
exec => [
'/usr/lib/apt/apt-helper', 'cat-file',
$info->{main}->{$opt_buildarch}
],
to_file => $tmp_buildarch_file_main,
wait_child => 1
);
push(@ceve_cmd, "deb://$tmp_buildarch_file_main");
}
if ($opt_buildarch ne $opt_hostarch) {
push(@ceve_cmd,
"--deb-host-arch=$opt_hostarch",
"deb://$hostarch_file");
spawn(
exec =>
['/usr/lib/apt/apt-helper', 'cat-file', $hostarch_file],
to_file => $tmp_hostarch_file,
wait_child => 1
);
if ($comp ne "main") {
# if this is not "main", also add "main" to the mix, to resolve
# dependencies correctly
(undef, my $tmp_hostarch_file_main) = tempfile(
'Packages_host_main.XXXXXX',
OPEN => 0,
DIR => $tmpdir
);
spawn(
exec => [
'/usr/lib/apt/apt-helper', 'cat-file',
$info->{main}->{$opt_hostarch}
],
to_file => $tmp_hostarch_file_main,
wait_child => 1
);
push(@ceve_cmd, "deb://$tmp_hostarch_file_main");
}
}
push(@ceve_cmd, "--deb-drop-b-d-indep") if ($opt_noarchall);
push(@ceve_cmd, "--deb-drop-b-d-arch") if ($opt_noarchany);
my %sources;
debug 'executing: ' . join(' ', @ceve_cmd);
open(SOURCES, '-|', @ceve_cmd);
while (<SOURCES>) {
next unless s/^Package:\s+//;
chomp;
$sources{$_} = 1;
}
for my $source (sort keys %sources) {
if ($opt_noftbfs && exists $ftbfs{$source}) {
next;
}
print $source;
if ($opt_maintainer) {
my $maintainer
= `apt-cache showsrc $source | grep-dctrl -n -s Maintainer '' | sort -u`;
print " ($maintainer)";
}
print "\n";
$count += 1;
}
} else {
open(my $out, '-|', '/usr/lib/apt/apt-helper', 'cat-file',
$source_file)
or die
"$progname: Unable to run \"apt-helper cat-file '$source_file'\": $!";
my %rdeps;
until (eof $out) {
my $ctrl = Dpkg::Control->new(type => CTRL_INDEX_SRC);
if (!$ctrl->parse($out, 'apt-helper cat-file')) {
next;
}
print STDERR "$ctrl\n" if ($opt_debug);
foreach my $package (@packages) {
for my $relation (
qw(Build-Depends Build-Depends-Indep Build-Depends-Arch)) {
if (exists $ctrl->{$relation}) {
if ($ctrl->{$relation}
=~ m/^(.*\s)?\Q$package\E(?::[a-zA-Z0-9][a-zA-Z0-9-]*)?([\s,]|$)/
) {
$rdeps{ $ctrl->{Package} }{Maintainer}
= $ctrl->{Maintainer};
}
}
}
}
}
close($out);
while (my $depending_package = each(%rdeps)) {
if ($opt_noftbfs && exists $ftbfs{$depending_package}) {
next;
}
print $depending_package;
if ($opt_maintainer) {
print " ($rdeps{$depending_package}->{'Maintainer'})";
}
print "\n";
$count += 1;
}
}
if (!$opt_quiet) {
if ($count == 0) {
print( "No reverse build-depends found for "
. (join ', ', @packages)
. ".\n\n");
} else {
print( "\nFound a total of $count reverse build-depend(s) for "
. (join ', ', @packages)
. ".\n\n");
}
}
}
if ($#ARGV < 0) { usage; exit(0); }
GetOptions(
"u|update" => \$opt_update,
"s|sudo" => \$opt_sudo,
"m|print-maintainer" => \$opt_maintainer,
"distribution=s" => \$opt_distribution,
"only-main" => \$opt_mainonly,
"only-devel" => \$opt_develonly,
"exclude-component=s" => \@opt_exclude_components,
"origin=s" => \$opt_origin,
"host-arch=s" => \$opt_hostarch,
"build-arch=s" => \$opt_buildarch,
"no-arch-all" => \$opt_noarchall,
"no-arch-any" => \$opt_noarchany,
"no-ftbfs" => \$opt_noftbfs,
# "profiles=s" => \$opt_profiles, # FIXME: add build profile support
# once dose-ceve has a
# --deb-profiles option
"old" => \$opt_without_ceve,
"q|quiet" => \$opt_quiet,
"d|debug" => \$opt_debug,
"h|help" => sub { usage; },
"v|version" => sub { version; }) or do { usage; exit 1; };
my @packages = @ARGV;
if (scalar @packages == 0) {
die "$progname: missing argument. expecting packagename\n";
}
foreach my $package (@packages) {
debug "Package => $package";
}
if ($opt_hostarch) {
if ($opt_without_ceve) {
die
"$progname: the --host-arch option cannot be used together with --old\n";
}
if (test_ceve()) {
$use_ceve = 1;
} else {
die
"$progname: the --host-arch option requires dose-extra >= 4.0 to be installed\n";
}
}
if ($opt_buildarch) {
if ($opt_without_ceve) {
die
"$progname: the --build-arch option cannot be used together with --old\n";
}
if (test_ceve()) {
$use_ceve = 1;
} else {
die
"$progname: the --build-arch option requires dose-extra >= 4.0 to be installed\n";
}
}
# if ceve usage has not been activated yet, check if it can be activated
if (!$use_ceve and !$opt_without_ceve) {
if (test_ceve()) {
$use_ceve = 1;
} else {
print STDERR
"WARNING: dose-extra >= 4.0 is not installed. Falling back to old unreliable behaviour.\n";
}
}
if ($use_ceve) {
if (!find_command('grep-dctrl')) {
die
"$progname: Fatal error. grep-dctrl is not available.\nPlease install the 'dctrl-tools' package.\n";
}
debug 'running with dose-ceve resolver';
} else {
debug 'running with old resolver';
}
# set hostarch and buildarch if they have not been set yet
if (!$opt_hostarch) {
$opt_hostarch = `dpkg-architecture --query DEB_HOST_ARCH`;
chomp $opt_hostarch;
}
if (!$opt_buildarch) {
$opt_buildarch = `dpkg-architecture --query DEB_BUILD_ARCH`;
chomp $opt_buildarch;
}
debug "buildarch=$opt_buildarch hostarch=$opt_hostarch";
if ($opt_update) {
debug 'Updating apt-cache before search';
my @cmd;
if ($opt_sudo) {
debug 'Using sudo to become root';
push(@cmd, 'sudo');
}
push(@cmd, 'apt-get', 'update');
system @cmd;
}
my $file_info = collect_files();
if (!%{$file_info}) {
die
"$progname: unable to find sources files.\nDid you forget to run apt-get update (or add --update to this command)?";
}
foreach my $site (sort keys %{$file_info}) {
foreach my $suite (sort keys %{ $file_info->{$site} }) {
foreach my $comp (qw(main contrib non-free non-free-firmware)) {
next unless exists $file_info->{$site}{$suite}{$comp};
my $skipmsg = "I: skipping $site $suite $comp because";
if (!exists $file_info->{$site}{$suite}{$comp}->{sources}) {
if (!$opt_quiet) {
print STDERR "$skipmsg Sources is missing\n";
}
next;
}
if (!exists $file_info->{$site}{$suite}{$comp}->{$opt_hostarch}) {
if (!$opt_quiet) {
print STDERR "$skipmsg binary-$opt_hostarch is missing\n";
}
next;
}
if (!exists $file_info->{$site}{$suite}{$comp}->{$opt_buildarch}) {
if (!$opt_quiet) {
print STDERR "$skipmsg binary-$opt_buildarch is missing\n";
}
next;
}
# for all components that are not "main", the component "main"
# must exist as well for the build and host architectures
if ($comp ne "main") {
$skipmsg .= " for associated component \"main\",";
if (!exists $file_info->{$site}{$suite}{"main"}
->{$opt_hostarch}) {
if (!$opt_quiet) {
print STDERR
"$skipmsg binary-$opt_hostarch is missing\n";
}
next;
}
if (!exists $file_info->{$site}{$suite}{"main"}
->{$opt_buildarch}) {
if (!$opt_quiet) {
print STDERR
"$skipmsg binary-$opt_buildarch is missing\n";
}
next;
}
}
if (!$opt_quiet) {
my $msg = "Reverse Build-depends in $suite/$comp:";
print STDERR "$msg\n";
print STDERR "-" x length($msg) . "\n\n";
}
findreversebuilddeps($file_info->{$site}{$suite}, $comp,
@packages);
}
}
}
=head1 LICENSE
This code is copyright by Patrick Schoenfeld
<schoenfeld@debian.org>, all rights reserved.
This program comes with ABSOLUTELEY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
=head1 AUTHOR
Patrick Schoenfeld <schoenfeld@debian.org>
=cut

View file

@ -0,0 +1,60 @@
# /usr/share/bash-completion/completions/chdist
# Bash command completion for chdist(1).
# Documentation: bash(1), section “Programmable Completion”.
_chdist ()
{
local cur=$2 prev=$3
local options='--help -h --data-dir -d --arch -a'
local commands='create apt apt-get apt-cache apt-rdepends aptitude
src2bin bin2src
compare-packages compare-bin-packages
compare-versions compare-bin-versions
grep-dctrl-packages grep-dctrl-sources
list'
# Sync'd with buildd.debian.org on 2016-04-02:
local archs="all alpha amd64 arm64 armel armhf hppa hurd-i386 i386 ia64 kfreebsd-amd64 kfreebsd-i386 m68k mips mips64el mipsel powerpc powerpcspe ppc64 ppc64el s390 s390x sh4 sparc sparc64 x32"
local dists=$(ls ~/.chdist 2>/dev/null)
COMPREPLY=()
case "$prev" in
-@(-arch|a))
COMPREPLY=( $( compgen -W "$archs" -- $cur ) )
return 0
;;
-@(-data-dir|d))
_filedir
return 0
;;
-@(-help|h)|list)
return 0
;;
create|apt|apt-get|apt-cache|apt-rdepends|aptitude|src2bin|bin2src|compare-packages|compare-bin-packages|compare-versions|compare-bin-versions|grep-dctrl-packages|grep-dctrl-sources)
COMPREPLY=( $( compgen -W "$dists" -- $cur ) )
return 0
esac
if [[ "$cur" == -* ]]; then
# return one of the possible options
COMPREPLY=( $( compgen -W "$options" -- $cur ) )
else
# return one of the possible commands
COMPREPLY=( $( compgen -W "$commands" -- $cur ) )
fi
return 0
}
complete -F _chdist chdist
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

789
scripts/chdist.pl Executable file
View file

@ -0,0 +1,789 @@
#!/usr/bin/perl
# Debian GNU/Linux chdist. Copyright (C) 2007 Lucas Nussbaum and Luk Claes.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
=head1 NAME
chdist - script to easily play with several distributions
=head1 SYNOPSIS
B<chdist> [I<options>] [I<command>] [I<command parameters>]
B<chdist> [I<options>] I<DIST> I<command> [I<command parameters>]
The second syntax is accepted when the I<DIST> does not match
one of the known commands from the list below (see L</COMMANDS>).
Then the I<command> may be any program available on the system
and anything based on apt will be using the I<DIST> apt data.
=head1 DESCRIPTION
B<chdist> is a rewrite of what used to be known as 'MultiDistroTools'
(or mdt). Its use is to create 'APT trees' for several distributions,
making it easy to query the status of packages in other distribution
without using chroots, for instance.
=head1 OPTIONS
=over 4
=item B<-h>, B<--help>
Provide a usage message.
=item B<-d>, B<--data-dir> I<DIR>
Choose data directory (default: F<~/.chdist/>).
=item B<-a>, B<--arch> I<ARCH>
Choose architecture (default: `B<dpkg --print-architecture>`).
=item B<--version>
Display version information.
=back
=head1 COMMANDS
=over 4
=item B<create> I<DIST> [I<URL> I<RELEASE> I<SECTIONS>]
Prepare a new tree named I<DIST>
=item B<apt> I<DIST> <B<update>|B<source>|B<show>|B<showsrc>|...>
Run B<apt> inside I<DIST>
=item B<apt-get> I<DIST> <B<update>|B<source>|...>
Run B<apt-get> inside I<DIST>
=item B<apt-cache> I<DIST> <B<show>|B<showsrc>|...>
Run B<apt-cache> inside I<DIST>
=item B<apt-file> I<DIST> <B<update>|B<search>|...>
Run B<apt-file> inside I<DIST>
=item B<apt-rdepends> I<DIST> [...]
Run B<apt-rdepends> inside I<DIST>
=item B<build-rdeps> I<DIST> [...]
Run B<build-rdeps> inside I<DIST>.
When the I<DIST> origin and suite/codename
differ from the system origin and suite/codename
then they need to be set using B<build-rdeps> options.
=item B<aptitude> I<DIST> [...]
Run B<aptitude> inside I<DIST>
=item B<src2bin> I<DIST SRCPKG>
List binary packages for I<SRCPKG> in I<DIST>
=item B<bin2src> I<DIST BINPKG>
List source package for I<BINPKG> in I<DIST>
=item B<compare-packages> I<DIST1 DIST2> [I<DIST3>, ...]
=item B<compare-bin-packages> I<DIST1 DIST2> [I<DIST3>, ...]
List versions of packages in several I<DIST>ributions
=item B<compare-versions> I<DIST1 DIST2>
=item B<compare-bin-versions> I<DIST1 DIST2>
Same as B<compare-packages>/B<compare-bin-packages>, but also runs
B<dpkg --compare-versions> and display where the package is newer.
=item B<compare-src-bin-packages> I<DIST>
Compare sources and binaries for I<DIST>
=item B<compare-src-bin-versions> I<DIST>
Same as B<compare-src-bin-packages>, but also run B<dpkg --compare-versions>
and display where the package is newer
=item B<grep-dctrl-packages> I<DIST> [...]
Run B<grep-dctrl> on F<*_Packages> inside I<DIST>
=item B<grep-dctrl-sources> I<DIST> [...]
Run B<grep-dctrl> on F<*_Sources> inside I<DIST>
=item B<list>
List available I<DIST>s
=back
=head1 COPYRIGHT
This program is copyright 2007 by Lucas Nussbaum and Luk Claes. This
program comes with ABSOLUTELY NO WARRANTY.
It is licensed under the terms of the GPL, either version 2 of the
License, or (at your option) any later version.
=cut
use strict;
use warnings;
use File::Copy qw(cp);
use File::HomeDir;
use File::Path qw(make_path);
use File::Basename;
use Getopt::Long qw(:config gnu_compat bundling require_order);
use Cwd qw(abs_path cwd);
use Dpkg::Version qw(version_compare);
use Pod::Usage;
# Redefine Pod::Text's cmd_i so pod2usage converts I<...> to <...> instead of
# *...*
{
package Pod::Text;
no warnings qw(redefine);
sub cmd_i { '<' . $_[2] . '>' }
}
my $progname = basename($0);
sub usage {
pod2usage(
-verbose => 99,
-exitval => $_[0],
-sections => 'SYNOPSIS|OPTIONS|ARGUMENTS|COMMANDS'
);
}
# specify the options we accept and initialize
# the option parser
my $help = '';
my $version = '';
my $versioninfo = <<"EOF";
This is $progname, from the Debian devscripts package, version
###VERSION### This code is copyright 2007 by Lucas Nussbaum and Luk
Claes. This program comes with ABSOLUTELY NO WARRANTY. You are free
to redistribute this code under the terms of the GNU General Public
License, version 2 or (at your option) any later version.
EOF
my $arch;
my $datadir = File::HomeDir->my_home . '/.chdist';
GetOptions(
"h|help" => \$help,
"d|data-dir=s" => \$datadir,
"a|arch=s" => \$arch,
"version" => \$version,
) or usage(1);
# Fix-up relative paths
$datadir = cwd() . "/$datadir" if $datadir !~ m!^/!;
$datadir = abs_path($datadir);
if ($help) {
usage(0);
}
if ($version) {
print $versioninfo;
exit 0;
}
########################################################
### Functions
########################################################
sub fatal {
my ($msg) = @_;
$msg =~ s/\n?$/\n/;
print STDERR "$progname: $msg";
exit 1;
}
sub uniq (@) {
my %hash;
map { $hash{$_}++ == 0 ? $_ : () } @_;
}
sub dist_check {
# Check that dist exists in $datadir
my ($dist) = @_;
if ($dist) {
my $dir = "$datadir/$dist";
return 0 if (-d $dir);
fatal(
"Could not find $dist in $datadir. Run `$progname create $dist` first."
);
} else {
fatal('No dist provided.');
}
}
sub type_check {
my ($type) = @_;
if (($type ne 'Sources') && ($type ne 'Packages')) {
fatal("Unknown type $type.");
}
}
sub aptopts {
# Build apt options
my ($dist) = @_;
my @opts = ();
if ($arch) {
print "W: Forcing arch $arch for this command only.\n";
push(@opts, '-o', "Apt::Architecture=$arch");
push(@opts, '-o', "Apt::Architectures=$arch");
}
return @opts;
}
sub aptconfig {
# Build APT_CONFIG override
my ($dist) = @_;
my $aptconf = "$datadir/$dist/etc/apt/apt.conf";
if (!-r $aptconf) {
fatal("Unable to read $aptconf");
}
$ENV{'APT_CONFIG'} = $aptconf;
}
###
sub aptcmd {
my ($cmd, $dist, @args) = @_;
dist_check($dist);
unshift(@args, aptopts($dist));
aptconfig($dist);
exec($cmd, @args);
}
sub apt_file {
my ($dist, @args) = @_;
dist_check($dist);
aptconfig($dist);
my @query = ('dpkg-query', '-W', '-f');
open(my $fd, '-|', @query, '${Version}', 'apt-file')
or fatal('Unable to run dpkg-query.');
my $aptfile_version = <$fd>;
close($fd);
if (version_compare('3.0~', $aptfile_version) < 0) {
open($fd, '-|', @query, '${Conffiles}\n', 'apt-file')
or fatal('Unable to run dpkg-query.');
my @aptfile_confs = map { (split)[0] }
grep { /apt\.conf\.d/ } <$fd>;
close($fd);
# New-style apt-file
for my $conffile (@aptfile_confs) {
if (!-f "$datadir/$dist/$conffile") {
cp($conffile, "$datadir/$dist/$conffile");
}
}
} else {
my $cache_directory
= $datadir . '/' . $dist . "/var/cache/apt/apt-file";
unshift(@args, '--cache', $cache_directory);
}
exec('apt-file', @args);
}
sub bin2src {
my ($dist, $pkg) = @_;
dist_check($dist);
if (!defined($pkg)) {
fatal("No package name provided. Exiting.");
}
my @args = (aptopts($dist), 'show', $pkg);
aptconfig($dist);
my $src = $pkg;
my $pid = open(CACHE, '-|', 'apt-cache', @args);
if (!defined($pid)) {
fatal("Couldn't run apt-cache: $!");
}
if ($pid) {
while (<CACHE>) {
if (m/^Source: (.*)/) {
$src = $1;
# Slurp remaining output to avoid SIGPIPE
local $/ = undef;
my $junk = <CACHE>;
last;
}
}
close CACHE || fatal("bad apt-cache $!: $?");
print "$src\n";
}
}
sub src2bin {
my ($dist, $pkg) = @_;
dist_check($dist);
if (!defined($pkg)) {
fatal("no package name provided. Exiting.");
}
my @args = (aptopts($dist), 'showsrc', $pkg);
aptconfig($dist);
my $pid = open(CACHE, '-|', 'apt-cache', @args);
if (!defined($pid)) {
fatal("Couldn't run apt-cache: $!");
}
if ($pid) {
while (<CACHE>) {
if (m/^Binary: (.*)/) {
print join("\n", split(/, /, $1)) . "\n";
# Slurp remaining output to avoid SIGPIPE
local $/ = undef;
my $junk = <CACHE>;
last;
}
}
close CACHE || fatal("bad apt-cache $!: $?");
}
}
sub dist_create {
my ($dist, $method, $version, @sections) = @_;
if (!defined($dist)) {
fatal("you must provide a dist name.");
}
my $dir = "$datadir/$dist";
if (-d $dir) {
fatal("$dir already exists, exiting.");
}
make_path($datadir);
foreach my $d ((
'/etc/apt', '/etc/apt/apt.conf.d',
'/etc/apt/preferences.d', '/etc/apt/trusted.gpg.d',
'/etc/apt/sources.list.d', '/var/lib/apt/lists/partial',
'/var/cache/apt/archives/partial', '/var/lib/dpkg'
)
) {
make_path("$dir/$d");
}
# Create sources.list
open(FH, '>', "$dir/etc/apt/sources.list");
if ($version) {
# Use provided method, version and sections
my $sections_str = join(' ', @sections);
print FH <<EOF;
deb $method $version $sections_str
deb-src $method $version $sections_str
EOF
} else {
if ($method) {
warn
"W: method provided without a section. Using default content for sources.list\n";
}
# Fill in sources.list with example contents
print FH <<EOF;
#deb http://deb.debian.org/debian/ unstable main contrib non-free non-free-firmware
#deb-src http://deb.debian.org/debian/ unstable main contrib non-free non-free-firmware
#deb http://archive.ubuntu.com/ubuntu jammy main universe restricted multiverse
#deb-src http://archive.ubuntu.com/ubuntu jammy main universe restricted multiverse
EOF
}
close FH;
# Create dpkg status
open(FH, '>', "$dir/var/lib/dpkg/status");
close FH; #empty file
# Create apt.conf
$arch ||= `dpkg --print-architecture`;
chomp $arch;
open(FH, ">$dir/etc/apt/apt.conf");
print FH <<EOF;
Apt {
Architecture "$arch";
Architectures "$arch";
};
Dir "$dir";
EOF
close FH;
foreach my $keyring (
qw(debian-archive-keyring.gpg
debian-archive-removed-keys.gpg
kali-archive-keyring.gpg
ubuntu-archive-keyring.gpg
ubuntu-archive-removed-keys.gpg)
) {
my $src = "/usr/share/keyrings/$keyring";
if (-f $src) {
symlink $src, "$dir/etc/apt/trusted.gpg.d/$keyring";
}
}
print "Now edit $dir/etc/apt/sources.list\n" unless $version;
print "Run chdist apt $dist update\n";
print "And enjoy.\n";
}
sub get_distfiles {
# Retrieve files to be read
# Takes a dist and a type
my ($dist, $type) = @_;
my @files;
foreach
my $file (glob($datadir . '/' . $dist . "/var/lib/apt/lists/*_$type")) {
if (-f $file) {
push @files, $file;
}
}
return \@files;
}
sub dist_compare(\@$$) {
# Takes a list of dists, a type of comparison and a do_compare flag
my ($dists, $do_compare, $type) = @_;
type_check($type);
# Get the list of dists from the reference
my @dists = @$dists;
map { dist_check($_) } @dists;
# Get all packages
my %packages;
foreach my $dist (@dists) {
my $files = get_distfiles($dist, $type);
my @files = @$files;
foreach my $file (@files) {
my $parsed_file = parseFile($file);
foreach my $package (keys(%{$parsed_file})) {
if ($packages{$dist}{$package}) {
my $version = $packages{$dist}{$package}{Version};
my $alt_ver = $parsed_file->{$package}{Version};
my $delta
= $version
&& $alt_ver
&& version_compare($version, $alt_ver);
if (defined($delta) && $delta < 0) {
$packages{$dist}{$package} = $parsed_file->{$package};
} else {
warn
"W: Package $package is already listed for $dist. Not overriding.\n";
}
} else {
$packages{$dist}{$package} = $parsed_file->{$package};
}
}
}
}
# Get entire list of packages
my @all_packages = uniq sort (map { keys(%{ $packages{$_} }) } @dists);
foreach my $package (@all_packages) {
my $line = "$package ";
my $status = "";
my $details;
foreach my $dist (@dists) {
if ($packages{$dist}{$package}) {
$line .= "$packages{$dist}{$package}{'Version'} ";
} else {
$line .= "UNAVAIL ";
$status = "not_in_$dist";
}
}
my @versions = map { $packages{$_}{$package}{'Version'} } @dists;
# Escaped versions
my @esc_vers = @versions;
foreach my $vers (@esc_vers) {
$vers =~ s|\+|\\\+| if defined $vers;
}
# Do compare
if ($do_compare) {
if (!@dists) {
fatal('Can only compare versions if there are two distros.');
}
if (!$status) {
my $cmp = version_compare($versions[0], $versions[1]);
if (!$cmp) {
$status = "same_version";
} elsif ($cmp < 0) {
$status = "newer_in_$dists[1]";
if ($versions[1] =~ m|^$esc_vers[0]|) {
$details = " local_changes_in_$dists[1]";
}
} else {
$status = "newer_in_$dists[0]";
if ($versions[0] =~ m|^$esc_vers[1]|) {
$details = " local_changes_in_$dists[0]";
}
}
}
$line .= " $status $details";
}
print "$line\n";
}
}
sub compare_src_bin {
my ($dist, $do_compare) = @_;
dist_check($dist);
# Get all packages
my %packages;
my @parse_types = ('Sources', 'Packages');
my @comp_types = ('Sources_Bin', 'Packages');
foreach my $type (@parse_types) {
my $files = get_distfiles($dist, $type);
my @files = @$files;
foreach my $file (@files) {
my $parsed_file = parseFile($file);
foreach my $package (keys(%{$parsed_file})) {
if ($packages{$dist}{$package}) {
warn
"W: Package $package is already listed for $dist. Not overriding.\n";
} else {
$packages{$type}{$package} = $parsed_file->{$package};
}
}
}
}
# Build 'Sources_Bin' hash
foreach my $package (keys(%{ $packages{Sources} })) {
my $package_h = \%{ $packages{Sources}{$package} };
if ($package_h->{'Binary'}) {
my @binaries = split(", ", $package_h->{'Binary'});
my $version = $package_h->{'Version'};
foreach my $binary (@binaries) {
if (defined $packages{Sources_Bin}{$binary}) {
my $alt_ver = $packages{Sources_Bin}{$binary}{Version};
# Skip this entry if it's an older version than we already
# have
if (version_compare($version, $alt_ver) < 0) {
next;
}
}
$packages{Sources_Bin}{$binary}{Version} = $version;
}
} else {
warn "Source $package has no binaries!\n";
}
}
# Get entire list of packages
my @all_packages
= uniq sort (map { keys(%{ $packages{$_} }) } @comp_types);
foreach my $package (@all_packages) {
my $line = "$package ";
my $status = "";
my $details = '';
foreach my $type (@comp_types) {
if ($packages{$type}{$package}) {
$line .= "$packages{$type}{$package}{'Version'} ";
} else {
$line .= "UNAVAIL ";
$status = "not_in_$type";
}
}
my @versions = map { $packages{$_}{$package}{'Version'} } @comp_types;
# Do compare
if ($do_compare) {
if (!@comp_types) {
fatal('Can only compare versions if there are two types.');
}
if (!$status) {
my $cmp = version_compare($versions[0], $versions[1]);
if (!$cmp) {
$status = "same_version";
} elsif ($cmp < 0) {
$status = "newer_in_$comp_types[1]";
if ($versions[1] =~ m|^\Q$versions[0]\E|) {
$details = " local_changes_in_$comp_types[1]";
}
} else {
$status = "newer_in_$comp_types[0]";
if ($versions[0] =~ m|^\Q$versions[1]\E|) {
$details = " local_changes_in_$comp_types[0]";
}
}
}
$line .= " $status $details";
}
print "$line\n";
}
}
sub grep_file(\@$) {
my ($argv, $file) = @_;
my $dist = shift @{$argv};
dist_check($dist);
my @f = glob($datadir . '/' . $dist . "/var/lib/apt/lists/*_$file");
if (@f) {
exec('grep-dctrl', @{$argv}, @f);
} else {
fatal("Couldn't find a $file for $dist.");
}
}
sub list {
opendir(DIR, $datadir) or fatal("can't open dir $datadir: $!");
while (my $file = readdir(DIR)) {
if ((-d "$datadir/$file") && ($file =~ m|^\w+|)) {
print "$file\n";
}
}
closedir(DIR);
}
sub parseFile {
my ($file) = @_;
# Parse a source file and returns results as a hash
open(FILE, '<', $file) || fatal("Could not open $file : $!");
# Use %tmp hash to store tmp data
my %tmp;
my %result;
while (my $line = <FILE>) {
if ($line =~ m|^$|) {
# Commit data if empty line
if ($tmp{'Package'}) {
#print "Committing data for $tmp{'Package'}\n";
while (my ($field, $data) = each(%tmp)) {
if ($field ne "Package") {
$result{ $tmp{'Package'} }{$field} = $data;
}
}
# Reset %tmp
%tmp = ();
} else {
warn "W: No Package field found. Not committing data.\n";
}
} elsif ($line =~ m|^[a-zA-Z]|) {
# Gather data
my ($field, $data) = $line =~ m|([a-zA-Z-]+): (.*)$|;
if ($data) {
$tmp{$field} = $data;
}
}
}
close(FILE);
return \%result;
}
########################################################
### Command parsing
########################################################
my $recursed = 0;
MAIN:
my $command = shift @ARGV;
usage(1) unless $command;
if ($command eq 'create') {
dist_create(@ARGV);
} elsif ($command eq 'apt') {
aptcmd('apt', @ARGV);
} elsif ($command eq 'apt-get') {
aptcmd('apt-get', @ARGV);
} elsif ($command eq 'apt-cache') {
aptcmd('apt-cache', @ARGV);
} elsif ($command eq 'apt-file') {
apt_file(@ARGV);
} elsif ($command eq 'apt-rdepends') {
aptcmd('apt-rdepends', @ARGV);
} elsif ($command eq 'build-rdeps') {
aptcmd('build-rdeps', @ARGV);
} elsif ($command eq 'aptitude') {
aptcmd('aptitude', @ARGV);
} elsif ($command eq 'synaptic') {
$ENV{PATH} .= ':/usr/sbin';
aptcmd('synaptic', @ARGV);
} elsif ($command eq 'bin2src') {
bin2src(@ARGV);
} elsif ($command eq 'src2bin') {
src2bin(@ARGV);
} elsif ($command eq 'compare-packages') {
dist_compare(@ARGV, 0, 'Sources');
} elsif ($command eq 'compare-bin-packages') {
dist_compare(@ARGV, 0, 'Packages');
} elsif ($command eq 'compare-versions') {
dist_compare(@ARGV, 1, 'Sources');
} elsif ($command eq 'compare-bin-versions') {
dist_compare(@ARGV, 1, 'Packages');
} elsif ($command eq 'grep-dctrl-packages') {
grep_file(@ARGV, 'Packages');
} elsif ($command eq 'grep-dctrl-sources') {
grep_file(@ARGV, 'Sources');
} elsif ($command eq 'compare-src-bin-packages') {
compare_src_bin(@ARGV, 0);
} elsif ($command eq 'compare-src-bin-versions') {
compare_src_bin(@ARGV, 1);
} elsif ($command eq 'list') {
list;
} else {
my $dist = $recursed ? shift @ARGV : $command;
$command = $recursed ? $command : shift @ARGV;
if ($dist) {
my $dir = "$datadir/$dist";
if (-d $dir) {
if (!$recursed) {
unshift @ARGV, $dist;
unshift @ARGV, $command;
$recursed = 1;
goto MAIN;
} else {
aptcmd($command, $dist, @ARGV);
}
} else {
if ($command eq 'create') {
dist_create($dist, @ARGV);
} else {
dist_check($dist);
}
}
} else {
usage(1);
}
}

77
scripts/checkbashisms.1 Normal file
View file

@ -0,0 +1,77 @@
.TH CHECKBASHISMS 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
checkbashisms \- check for bashisms in /bin/sh scripts
.SH SYNOPSIS
\fBcheckbashisms\fR \fIscript\fR ...
.br
\fBcheckbashisms \-\-help\fR|\fB\-\-version\fR
.SH DESCRIPTION
\fBcheckbashisms\fR, based on one of the checks from the \fBlintian\fR
system, performs basic checks on \fI/bin/sh\fR shell scripts for the
possible presence of bashisms. It takes the names of the shell
scripts on the command line, and outputs warnings if possible bashisms
are detected.
.PP
Note that the definition of a bashism in this context roughly equates
to "a shell feature that is not required to be supported by POSIX"; this
means that some issues flagged may be permitted under optional sections
of POSIX, such as XSI or User Portability.
.PP
In cases where POSIX and Debian Policy disagree, \fBcheckbashisms\fR by
default allows extensions permitted by Policy but may also provide
options for stricter checking.
.SH OPTIONS
.TP
.BR \-\-help ", " \-h
Show a summary of options.
.TP
.BR \-\-newline ", " \-n
Check for "\fBecho \-n\fR" usage (non POSIX but required by Debian Policy 10.4.)
.TP
.BR \-\-posix ", " \-p
Check for issues which are non POSIX but required to be supported by Debian
Policy 10.4 (implies \fB\-n\fR).
.TP
.BR \-\-force ", " \-f
Force each script to be checked, even if it would normally not be (for
instance, it has a bash or non POSIX shell shebang or appears to be a
shell wrapper).
.TP
.BR \-\-lint ", " \-l
Act like a linter, for integration into a text editor. Possible
bashisms will be printed in stdout, like so:
.IP
.I {filename}:{lineno}:1: warning: possible bashism; {explanation}
.TP
.BR \-\-extra ", " \-x
Highlight lines which, whilst they do not contain bashisms, may be
useful in determining whether a particular issue is a false positive
which may be ignored.
For example, the use of "\fB$BASH_ENV\fR" may be preceded by checking
whether "\fB$BASH\fR" is set.
.TP
.BR \-\-early-fail ", " \-e
Exit right after a first error is seen.
.TP
.BR \-\-version ", " \-v
Show version and copyright information.
.SH "EXIT VALUES"
The exit value will be 0 if no possible bashisms or other problems
were detected. Otherwise it will be the sum of the following error
values:
.TP
1
A possible bashism was detected.
.TP
2
A file was skipped for some reason, for example, because it was
unreadable or not found. The warning message will give details.
.TP
4
No bashisms were detected in a bash script.
.SH "SEE ALSO"
.BR lintian (1)
.SH AUTHOR
\fBcheckbashisms\fR was originally written as a shell script by Yann Dirson
<\fIdirson@debian.org\fR> and rewritten in Perl with many more features by
Julian Gilbey <\fIjdg@debian.org\fR>.

View file

@ -0,0 +1,28 @@
# /usr/share/bash-completion/completions/checkbashisms
# Bash command completion for checkbashisms(1).
# Documentation: bash(1), section “Programmable Completion”.
# Copyright © 2015, Nicholas Bamber <nicholas@periapt.co.uk>
_checkbashisms()
{
local cur prev words cword special
_init_completion || return
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W '--newline --posix --force --extra --early-fail' -- "$cur" ) )
else
COMPREPLY=( $( compgen -o filenames -f -- "$cur" ) )
fi
return 0
} &&
complete -F _checkbashisms checkbashisms
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

822
scripts/checkbashisms.pl Executable file
View file

@ -0,0 +1,822 @@
#!/usr/bin/perl
# This script is essentially copied from /usr/share/lintian/checks/scripts,
# which is:
# Copyright (C) 1998 Richard Braakman
# Copyright (C) 2002 Josip Rodin
# This version is
# Copyright (C) 2003 Julian Gilbey
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use strict;
use warnings;
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use File::Temp qw/tempfile/;
sub init_hashes;
(my $progname = $0) =~ s|.*/||;
my $usage = <<"EOF";
Usage: $progname [-n] [-f] [-x] [-e] [-l] script ...
or: $progname --help
or: $progname --version
This script performs basic checks for the presence of bashisms
in /bin/sh scripts and the lack of bashisms in /bin/bash ones.
EOF
my $version = <<"EOF";
This is $progname, from the Debian devscripts package, version ###VERSION###
This code is copyright 2003 by Julian Gilbey <jdg\@debian.org>,
based on original code which is copyright 1998 by Richard Braakman
and copyright 2002 by Josip Rodin.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2, or (at your option) any later version.
EOF
my ($opt_echo, $opt_force, $opt_extra, $opt_posix, $opt_early_fail, $opt_lint);
my ($opt_help, $opt_version);
my @filenames;
# Detect if STDIN is a pipe
if (scalar(@ARGV) == 0 && (-p STDIN or -f STDIN)) {
push(@ARGV, '-');
}
##
## handle command-line options
##
$opt_help = 1 if int(@ARGV) == 0;
GetOptions(
"help|h" => \$opt_help,
"version|v" => \$opt_version,
"newline|n" => \$opt_echo,
"lint|l" => \$opt_lint,
"force|f" => \$opt_force,
"extra|x" => \$opt_extra,
"posix|p" => \$opt_posix,
"early-fail|e" => \$opt_early_fail,
)
or die
"Usage: $progname [options] filelist\nRun $progname --help for more details\n";
if ($opt_help) { print $usage; exit 0; }
if ($opt_version) { print $version; exit 0; }
$opt_echo = 1 if $opt_posix;
my $mode = 0;
my $issues = 0;
my $status = 0;
my $makefile = 0;
my (%bashisms, %string_bashisms, %singlequote_bashisms);
my $LEADIN
= qr'(?:(?:^|[`&;(|{])\s*|(?:(?:if|elif|while)(?:\s+!)?|then|do|shell)\s+)';
init_hashes;
my @bashisms_keys = sort keys %bashisms;
my @string_bashisms_keys = sort keys %string_bashisms;
my @singlequote_bashisms_keys = sort keys %singlequote_bashisms;
foreach my $filename (@ARGV) {
my $check_lines_count = -1;
my $display_filename = $filename;
if ($filename eq '-') {
my $tmp_fh;
($tmp_fh, $filename)
= tempfile("chkbashisms_tmp.XXXX", TMPDIR => 1, UNLINK => 1);
while (my $line = <STDIN>) {
print $tmp_fh $line;
}
close($tmp_fh);
$display_filename = "(stdin)";
}
if (!$opt_force) {
$check_lines_count = script_is_evil_and_wrong($filename);
}
if ($check_lines_count == 0 or $check_lines_count == 1) {
warn
"script $display_filename does not appear to be a /bin/sh script; skipping\n";
next;
}
if ($check_lines_count != -1) {
warn
"script $display_filename appears to be a shell wrapper; only checking the first "
. "$check_lines_count lines\n";
}
unless (open C, '<', $filename) {
warn "cannot open script $display_filename for reading: $!\n";
$status |= 2;
next;
}
$issues = 0;
$mode = 0;
my $cat_string = "";
my $cat_indented = 0;
my $quote_string = "";
my $last_continued = 0;
my $continued = 0;
my $found_rules = 0;
my $buffered_orig_line = "";
my $buffered_line = "";
my %start_lines;
while (<C>) {
next unless ($check_lines_count == -1 or $. <= $check_lines_count);
if ($. == 1) { # This should be an interpreter line
if (m,^\#!\s*(?:\S+/env\s+)?(\S+),) {
my $interpreter = $1;
if ($interpreter =~ m,(?:^|/)make$,) {
init_hashes if !$makefile++;
$makefile = 1;
} else {
init_hashes if $makefile--;
$makefile = 0;
}
next if $opt_force;
if ($interpreter =~ m,(?:^|/)bash$,) {
$mode = 1;
} elsif ($interpreter !~ m,(?:^|/)(sh|dash|posh)$,) {
### ksh/zsh?
warn
"script $display_filename does not appear to be a /bin/sh script; skipping\n";
$status |= 2;
last;
}
} else {
warn
"script $display_filename does not appear to have a \#! interpreter line;\nyou may get strange results\n";
}
}
chomp;
my $orig_line = $_;
# We want to remove end-of-line comments, so need to skip
# comments that appear inside balanced pairs
# of single or double quotes
# Remove comments in the "quoted" part of a line that starts
# in a quoted block? The problem is that we have no idea
# whether the program interpreting the block treats the
# quote character as part of the comment or as a quote
# terminator. We err on the side of caution and assume it
# will be treated as part of the comment.
# s/^(?:.*?[^\\])?$quote_string(.*)$/$1/ if $quote_string ne "";
# skip comment lines
if ( m,^\s*\#,
&& $quote_string eq ''
&& $buffered_line eq ''
&& $cat_string eq '') {
next;
}
# Remove quoted strings so we can more easily ignore comments
# inside them
s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
# If inside a quoted string, remove everything before the quote
s/^.+?\'//
if ($quote_string eq "'");
s/^.+?[^\\]\"//
if ($quote_string eq '"');
# If the remaining string contains what looks like a comment,
# eat it. In either case, swap the unmodified script line
# back in for processing.
if (m/(?:^|[^[\\])[\s\&;\(\)](\#.*$)/) {
$_ = $orig_line;
s/\Q$1\E//; # eat comments
} else {
$_ = $orig_line;
}
# Handle line continuation
if (!$makefile && $cat_string eq '' && m/\\$/) {
chop;
$buffered_line .= $_;
$buffered_orig_line .= $orig_line . "\n";
next;
}
if ($buffered_line ne '') {
$_ = $buffered_line . $_;
$orig_line = $buffered_orig_line . $orig_line;
$buffered_line = '';
$buffered_orig_line = '';
}
if ($makefile) {
$last_continued = $continued;
if (/[^\\]\\$/) {
$continued = 1;
} else {
$continued = 0;
}
# Don't match lines that look like a rule if we're in a
# continuation line before the start of the rules
if (/^[\w%-]+:+\s.*?;?(.*)$/
and !($last_continued and !$found_rules)) {
$found_rules = 1;
$_ = $1 if $1;
}
last
if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%;
# Remove "simple" target names
s/^[\w%.-]+(?:\s+[\w%.-]+)*::?//;
s/^\t//;
s/(?<!\$)\$\((\w+)\)/\${$1}/g;
s/(\$){2}/$1/g;
s/^[\s\t]*[@-]{1,2}//;
}
if (
$cat_string ne ""
&& (m/^\Q$cat_string\E$/
|| ($cat_indented && m/^\t*\Q$cat_string\E$/))
) {
$cat_string = "";
next;
}
my $within_another_shell = 0;
if (m,(^|\s+)((/usr)?/bin/)?((b|d)?a|k|z|t?c)sh\s+-c\s*.+,) {
$within_another_shell = 1;
}
# if cat_string is set, we are in a HERE document and need not
# check for things
if ($cat_string eq "" and !$within_another_shell) {
my $found = 0;
my $match = '';
my $explanation = '';
my $line = $_;
# Remove "" / '' as they clearly aren't quoted strings
# and not considering them makes the matching easier
$line =~ s/(^|[^\\])(\'\')+/$1/g;
$line =~ s/(^|[^\\])(\"\")+/$1/g;
if ($quote_string ne "") {
my $otherquote = ($quote_string eq "\"" ? "\'" : "\"");
# Inside a quoted block
if ($line =~ /(?:^|^.*?[^\\])$quote_string(.*)$/) {
my $rest = $1;
my $templine = $line;
# Remove quoted strings delimited with $otherquote
$templine
=~ s/(^|[^\\])$otherquote[^$quote_string]*?[^\\]$otherquote/$1/g;
# Remove quotes that are themselves quoted
# "a'b"
$templine
=~ s/(^|[^\\])$otherquote.*?$quote_string.*?[^\\]$otherquote/$1/g;
# "\""
$templine
=~ s/(^|[^\\])$quote_string\\$quote_string$quote_string/$1/g;
# After all that, were there still any quotes left?
my $count = () = $templine =~ /(^|[^\\])$quote_string/g;
next if $count == 0;
$count = () = $rest =~ /(^|[^\\])$quote_string/g;
if ($count % 2 == 0) {
# Quoted block ends on this line
# Ignore everything before the closing quote
$line = $rest || '';
$quote_string = "";
} else {
next;
}
} else {
# Still inside the quoted block, skip this line
next;
}
}
# Check even if we removed the end of a quoted block
# in the previous check, as a single line can end one
# block and begin another
if ($quote_string eq "") {
# Possible start of a quoted block
for my $quote ("\"", "\'") {
my $templine = $line;
my $otherquote = ($quote eq "\"" ? "\'" : "\"");
# Remove balanced quotes and their content
while (1) {
my ($length_single, $length_double) = (0, 0);
# Determine which one would match first:
if ($templine
=~ m/(^.+?(?:^|[^\\\"](?:\\\\)*)\')[^\']*\'/) {
$length_single = length($1);
}
if ($templine
=~ m/(^.*?(?:^|[^\\\'](?:\\\\)*)\")(?:\\.|[^\\\"])+\"/
) {
$length_double = length($1);
}
# Now simplify accordingly (shorter is preferred):
if (
$length_single != 0
&& ( $length_single < $length_double
|| $length_double == 0)
) {
$templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/;
} elsif ($length_double != 0) {
$templine
=~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/;
} else {
last;
}
}
# Don't flag quotes that are themselves quoted
# "a'b"
$templine =~ s/$otherquote.*?$quote.*?$otherquote//g;
# "\""
$templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/g;
# \' or \"
$templine =~ s/\\[\'\"]//g;
my $count = () = $templine =~ /(^|(?!\\))$quote/g;
# If there's an odd number of non-escaped
# quotes in the line it's almost certainly the
# start of a quoted block.
if ($count % 2 == 1) {
$quote_string = $quote;
$start_lines{'quote_string'} = $.;
$line =~ s/^(.*)$quote.*$/$1/;
last;
}
}
}
# since this test is ugly, I have to do it by itself
# detect source (.) trying to pass args to the command it runs
# The first expression weeds out '. "foo bar"'
if ( not $found
and not
m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/o
and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/o) {
if ($2 =~ /^(\&|\||\d?>|<)/) {
# everything is ok
;
} else {
$found = 1;
$match = $1;
$explanation = "sourced script with arguments";
output_explanation($display_filename, $orig_line,
$explanation);
}
}
# Remove "quoted quotes". They're likely to be inside
# another pair of quotes; we're not interested in
# them for their own sake and removing them makes finding
# the limits of the outer pair far easier.
$line =~ s/(^|[^\\\'\"])\"\'\"/$1/g;
$line =~ s/(^|[^\\\'\"])\'\"\'/$1/g;
foreach my $re (@singlequote_bashisms_keys) {
my $expl = $singlequote_bashisms{$re};
if ($line =~ m/($re)/) {
$found = 1;
$match = $1;
$explanation = $expl;
output_explanation($display_filename, $orig_line,
$explanation);
}
}
my $re = '(?<![\$\\\])\$\'[^\']+\'';
if ($line =~ m/(.*)($re)/o) {
my $count = () = $1 =~ /(^|[^\\])\'/g;
if ($count % 2 == 0) {
output_explanation($display_filename, $orig_line,
q<$'...' should be "$(printf '...')">);
}
}
# $cat_line contains the version of the line we'll check
# for heredoc delimiters later. Initially, remove any
# spaces between << and the delimiter to make the following
# updates to $cat_line easier. However, don't remove the
# spaces if the delimiter starts with a -, as that changes
# how the delimiter is searched.
my $cat_line = $line;
$cat_line =~ s/(<\<-?)\s+(?!-)/$1/g;
# Ignore anything inside single quotes; it could be an
# argument to grep or the like.
$line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
# As above, with the exception that we don't remove the string
# if the quote is immediately preceded by a < or a -, so we
# can match "foo <<-?'xyz'" as a heredoc later
# The check is a little more greedy than we'd like, but the
# heredoc test itself will weed out any false positives
$cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
$re = '(?<![\$\\\])\$\"[^\"]+\"';
if ($line =~ m/(.*)($re)/o) {
my $count = () = $1 =~ /(^|[^\\])\"/g;
if ($count % 2 == 0) {
output_explanation($display_filename, $orig_line,
q<$"foo" should be eval_gettext "foo">);
}
}
foreach my $re (@string_bashisms_keys) {
my $expl = $string_bashisms{$re};
if ($line =~ m/($re)/) {
$found = 1;
$match = $1;
$explanation = $expl;
output_explanation($display_filename, $orig_line,
$explanation);
}
}
# We've checked for all the things we still want to notice in
# double-quoted strings, so now remove those strings as well.
$line =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
$cat_line =~ s/(^|[^<\\\'-](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
foreach my $re (@bashisms_keys) {
my $expl = $bashisms{$re};
if ($line =~ m/($re)/) {
$found = 1;
$match = $1;
$explanation = $expl;
output_explanation($display_filename, $orig_line,
$explanation);
}
}
# This check requires the value to be compared, which could
# be done in the regex itself but requires "use re 'eval'".
# So it's better done in its own
if ($line =~ m/$LEADIN((?:exit|return)\s+(\d{3,}))/o && $2 > 255) {
$explanation = 'exit|return status code greater than 255';
output_explanation($display_filename, $orig_line,
$explanation);
}
# Only look for the beginning of a heredoc here, after we've
# stripped out quoted material, to avoid false positives.
if ($cat_line
=~ m/(?:^|[^<])\<\<(\-?)\s*(?:(?!<|'|")((?:[^\s;>|]+(?:(?<=\\)[\s;>|])?)+)|[\'\"](.*?)[\'\"])/
) {
$cat_indented = ($1 && $1 eq '-') ? 1 : 0;
my $quoted = defined($3);
$cat_string = $quoted ? $3 : $2;
unless ($quoted) {
# Now strip backslashes. Keep the position of the
# last match in a variable, as s/// resets it back
# to undef, but we don't want that.
my $pos = 0;
pos($cat_string) = $pos;
while ($cat_string =~ s/\G(.*?)\\/$1/) {
# position += length of match + the character
# that followed the backslash:
$pos += length($1) + 1;
pos($cat_string) = $pos;
}
}
$start_lines{'cat_string'} = $.;
}
}
}
warn
"error: $display_filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>, opened in line $start_lines{'cat_string'}\n"
if ($cat_string ne '');
warn
"error: $display_filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>, opened in line $start_lines{'quote_string'}\n"
if ($quote_string ne '');
warn "error: $display_filename: EOF reached while on line continuation.\n"
if ($buffered_line ne '');
close C;
if ($mode && !$issues) {
warn "could not find any possible bashisms in bash script $filename\n";
$status |= 4;
}
}
exit $status;
sub output_explanation {
my ($filename, $line, $explanation) = @_;
if ($mode) {
# When examining a bash script, just flag that there are indeed
# bashisms present
$issues = 1;
} else {
if ($opt_lint) {
print "$filename:$.:1: warning: possible bashism; $explanation\n";
} else {
warn
"possible bashism in $filename line $. ($explanation):\n$line\n";
}
if ($opt_early_fail) {
exit 1;
}
$status |= 1;
}
}
# Returns non-zero if the given file is not actually a shell script,
# just looks like one.
sub script_is_evil_and_wrong {
my ($filename) = @_;
my $ret = -1;
# lintian's version of this function aborts if the file
# can't be opened, but we simply return as the next
# test in the calling code handles reporting the error
# itself
open(IN, '<', $filename) or return $ret;
my $i = 0;
my $var = "0";
my $backgrounded = 0;
local $_;
while (<IN>) {
chomp;
next if /^#/o;
next if /^$/o;
last if (++$i > 55);
if (
m~
# the exec should either be "eval"ed or a new statement
(^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
# eat anything between the exec and $0
exec\s*.+\s*
# optionally quoted executable name (via $0)
.?\$$var.?\s*
# optional "end of options" indicator
(--\s*)?
# Match expressions of the form '${1+$@}', '${1:+"$@"',
# '"${1+$@', "$@", etc where the quotes (before the dollar
# sign(s)) are optional and the second (or only if the $1
# clause is omitted) parameter may be $@ or $*.
#
# Finally the whole subexpression may be omitted for scripts
# which do not pass on their parameters (i.e. after re-execing
# they take their parameters (and potentially data) from stdin
.?(\$\{1:?\+.?)?(\$(\@|\*))?~x
) {
$ret = $. - 1;
last;
} elsif (/^\s*(\w+)=\$0;/) {
$var = $1;
} elsif (
m~
# Match scripts which use "foo $0 $@ &\nexec true\n"
# Program name
\S+\s+
# As above
.?\$$var.?\s*
(--\s*)?
.?(\$\{1:?\+.?)?(\$(\@|\*))?.?\s*\&~x
) {
$backgrounded = 1;
} elsif (
$backgrounded
and m~
# the exec should either be "eval"ed or a new statement
(^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
exec\s+true(\s|\Z)~x
) {
$ret = $. - 1;
last;
} elsif (m~\@DPATCH\@~) {
$ret = $. - 1;
last;
}
}
close IN;
return $ret;
}
sub init_hashes {
%bashisms = (
qr'(?:^|\s+)function [^<>\(\)\[\]\{\};|\s]+(\s|\(|\Z)' =>
q<'function' is useless>,
$LEADIN . qr'select\s+\w+' => q<'select' is not POSIX>,
qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>,
qr'\s\|\&' => q<pipelining is not POSIX>,
qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
qr'\{\d+\.\.\d+(?:\.\.\d+)?\}' =>
q<brace expansion, {a..b[..c]}should be $(seq a [c] b)>,
qr'(?i)\{[a-z]\.\.[a-z](?:\.\.\d+)?\}' => q<brace expansion>,
qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>,
$LEADIN
. qr'read\s+(?:-[a-qs-zA-Z\d-]+)' =>
q<read with option other than -r>,
$LEADIN
. qr'read(?:\s+(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?)?\s*(?:;|$)' =>
q<read without variable>,
$LEADIN . qr'echo\s+(-n\s+)?-n?en?\s' => q<echo -e>,
$LEADIN . qr'exec\s+-[acl]' => q<exec -c/-l/-a name>,
$LEADIN . qr'let\s' => q<let ...>,
qr'(?<![\$\(])\(\(.*\)\)' => q<'((' should be '$(('>,
qr'(?:^|\s+)(\[|test)\s+-a' => q<test with unary -a (should be -e)>,
qr'\&>' => q<should be \>word 2\>&1>,
qr'(<\&|>\&)\s*((-|\d+)[^\s;|)}`&\\\\]|[^-\d\s]+(?<!\$)(?!\d))' =>
q<should be \>word 2\>&1>,
qr'\[\[(?!:)' =>
q<alternative test command ([[ foo ]] should be [ foo ])>,
qr'/dev/(tcp|udp)' => q</dev/(tcp|udp)>,
$LEADIN . qr'builtin\s' => q<builtin>,
$LEADIN . qr'caller\s' => q<caller>,
$LEADIN . qr'compgen\s' => q<compgen>,
$LEADIN . qr'complete\s' => q<complete>,
$LEADIN . qr'declare\s' => q<declare>,
$LEADIN . qr'dirs(\s|\Z)' => q<dirs>,
$LEADIN . qr'disown\s' => q<disown>,
$LEADIN . qr'enable\s' => q<enable>,
$LEADIN . qr'mapfile\s' => q<mapfile>,
$LEADIN . qr'readarray\s' => q<readarray>,
$LEADIN . qr'shopt(\s|\Z)' => q<shopt>,
$LEADIN . qr'suspend\s' => q<suspend>,
$LEADIN . qr'time\s' => q<time>,
$LEADIN . qr'type\s' => q<type>,
$LEADIN . qr'typeset\s' => q<typeset>,
$LEADIN . qr'ulimit(\s|\Z)' => q<ulimit>,
$LEADIN . qr'set\s+-[BHT]+' => q<set -[BHT]>,
$LEADIN . qr'alias\s+-p' => q<alias -p>,
$LEADIN . qr'unalias\s+-a' => q<unalias -a>,
$LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
# function '=' is special-cased due to bash arrays (think of "foo=()")
qr'(?:^|\s)\s*=\s*\(\s*\)\s*([\{|\(]|\Z)' =>
q<function names should only contain [a-z0-9_]>,
qr'(?:^|\s)(?<func>function\s)?\s*(?:[^<>\(\)\[\]\{\};|\s]*[^<>\(\)\[\]\{\};|\s\w][^<>\(\)\[\]\{\};|\s]*)(?(<func>)(?=)|(?<!=))\s*(?(<func>)(?:\(\s*\))?|\(\s*\))\s*([\{|\(]|\Z)'
=> q<function names should only contain [a-z0-9_]>,
$LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>,
$LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>,
qr'(?:^|\s+)[<>]\(.*?\)' => q<\<() process substitution>,
$LEADIN . qr'readonly\s+-[af]' => q<readonly -[af]>,
$LEADIN . qr'(sh|\$\{?SHELL\}?) -[rD]' => q<sh -[rD]>,
$LEADIN . qr'(sh|\$\{?SHELL\}?) --\w+' => q<sh --long-option>,
$LEADIN . qr'(sh|\$\{?SHELL\}?) [-+]O' => q<sh [-+]O>,
qr'\[\^[^]]+\]' => q<[^] should be [!]>,
$LEADIN
. qr'printf\s+-v' =>
q<'printf -v var ...' should be var='$(printf ...)'>,
$LEADIN . qr'coproc\s' => q<coproc>,
qr';;?&' => q<;;& and ;& special case operators>,
$LEADIN . qr'jobs\s' => q<jobs>,
# $LEADIN . qr'jobs\s+-[^lp]\s' => q<'jobs' with option other than -l or -p>,
$LEADIN
. qr'command\s+(?:-[pvV]+\s+)*-(?:[pvV])*[^pvV\s]' =>
q<'command' with option other than -p, -v or -V>,
$LEADIN
. qr'setvar\s' =>
q<setvar 'foo' 'bar' should be eval 'foo="'"$bar"'"'>,
$LEADIN
. qr'trap\s+["\']?.*["\']?\s+.*(?:ERR|DEBUG|RETURN)' =>
q<trap with ERR|DEBUG|RETURN>,
$LEADIN
. qr'(?:exit|return)\s+-\d' =>
q<exit|return with negative status code>,
$LEADIN
. qr'(?:exit|return)\s+--' =>
q<'exit --' should be 'exit' (idem for return)>,
$LEADIN . qr'hash(\s|\Z)' => q<hash>,
qr'(?:[:=\s])~(?:[+-]|[+-]?\d+)(?:[/\s]|\Z)' =>
q<non-standard tilde expansion>,
);
%string_bashisms = (
qr'\$\[[^][]+\]' => q<'$[' should be '$(('>,
qr'\$\{(?:\w+|@|\*)\:(?:\d+|\$\{?\w+\}?)+(?::(?:\d+|\$\{?\w+\}?)+)?\}'
=> q<${foo:3[:1]}>,
qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>,
qr'\$\{!\w+\}' => q<${!name}>,
qr'\$\{(?:\w+|@|\*)([,^]{1,2}.*?)\}' =>
q<${parm,[,][pat]} or ${parm^[^][pat]}>,
qr'\$\{[@*]([#%]{1,2}.*?)\}' => q<${[@|*]#[#]pat} or ${[@|*]%[%]pat}>,
qr'\$\{#[@*]\}' => q<${#@} or ${#*}>,
qr'\$\{(?:\w+|@|\*)(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
qr'\$\{\#?\w+\[.+\](?:[/,:#%^].+?)?\}' =>
q<bash arrays, ${name[0|*|@]}>,
qr'\$\{?RANDOM\}?\b' => q<$RANDOM>,
qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>,
qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
qr'\$\{?DIRSTACK\}?\b' => q<$DIRSTACK>,
qr'\$\{?EUID\}?\b' => q<$EUID should be "$(id -u)">,
qr'\$\{?UID\}?\b' => q<$UID should be "$(id -ru)">,
qr'\$\{?SECONDS\}?\b' => q<$SECONDS>,
qr'\$\{?BASH_[A-Z]+\}?\b' => q<$BASH_SOMETHING>,
qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>,
qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>,
qr'\$\{?SHLVL\}?\b' => q<$SHLVL>,
qr'\$\{?FUNCNAME\}?\b' => q<$FUNCNAME>,
qr'\$\{?TMOUT\}?\b' => q<$TMOUT>,
qr'(?:^|\s+)TMOUT=' => q<TMOUT=>,
qr'\$\{?TIMEFORMAT\}?\b' => q<$TIMEFORMAT>,
qr'(?:^|\s+)TIMEFORMAT=' => q<TIMEFORMAT=>,
qr'(?<![$\\])\$\{?_\}?\b' => q<$_>,
qr'(?:^|\s+)GLOBIGNORE=' => q<GLOBIGNORE=>,
qr'<<<' => q<\<\<\< here string>,
$LEADIN
. qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' =>
q<unsafe echo with backslash>,
qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' =>
q<'$((n++))' should be '$n; $((n=n+1))'>,
qr'\$\(\([\s\w$*/+-]*\+\+\w.*?\)\)' =>
q<'$((++n))' should be '$((n=n+1))'>,
qr'\$\(\([\s\w$*/+-]*\w\-\-.*?\)\)' =>
q<'$((n--))' should be '$n; $((n=n-1))'>,
qr'\$\(\([\s\w$*/+-]*\-\-\w.*?\)\)' =>
q<'$((--n))' should be '$((n=n-1))'>,
qr'\$\(\([\s\w$*/+-]*\*\*.*?\)\)' => q<exponentiation is not POSIX>,
$LEADIN . qr'printf\s["\'][^"\']*?%q.+?["\']' => q<printf %q>,
);
%singlequote_bashisms = (
$LEADIN
. qr'echo\s+(?:-[^e\s]+\s+)?\'[^\']*(\\[abcEfnrtv0])+.*?[\']' =>
q<unsafe echo with backslash>,
$LEADIN
. qr'source\s+[\"\']?(?:\.\/|\/|\$|[\w~.-])\S*' =>
q<should be '.', not 'source'>,
);
if ($opt_echo) {
$bashisms{ $LEADIN . qr'echo\s+-[A-Za-z]*n' } = q<echo -n>;
}
if ($opt_posix) {
$bashisms{ $LEADIN . qr'local\s+\w+(\s+\W|\s*[;&|)]|$)' }
= q<local foo>;
$bashisms{ $LEADIN . qr'local\s+\w+=' } = q<local foo=bar>;
$bashisms{ $LEADIN . qr'local\s+\w+\s+\w+' } = q<local x y>;
$bashisms{ $LEADIN . qr'((?:test|\[)\s+.+\s-[ao])\s' } = q<test -a/-o>;
$bashisms{ $LEADIN . qr'kill\s+-[^sl]\w*' } = q<kill -[0-9] or -[A-Z]>;
$bashisms{ $LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]' }
= q<trap with signal numbers>;
}
if ($makefile) {
$string_bashisms{qr'(\$\(|\`)\s*\<\s*([^\s\)]{2,}|[^DF])\s*(\)|\`)'}
= q<'$(\< foo)' should be '$(cat foo)'>;
} else {
$bashisms{ $LEADIN . qr'\w+\+=' } = q<should be VAR="${VAR}foo">;
$string_bashisms{qr'(\$\(|\`)\s*\<\s*\S+\s*(\)|\`)'}
= q<'$(\< foo)' should be '$(cat foo)'>;
}
if ($opt_extra) {
$string_bashisms{qr'\$\{?BASH\}?\b'} = q<$BASH>;
$string_bashisms{qr'(?:^|\s+)RANDOM='} = q<RANDOM=>;
$string_bashisms{qr'(?:^|\s+)(OS|MACH)TYPE='} = q<(OS|MACH)TYPE=>;
$string_bashisms{qr'(?:^|\s+)HOST(TYPE|NAME)='} = q<HOST(TYPE|NAME)=>;
$string_bashisms{qr'(?:^|\s+)DIRSTACK='} = q<DIRSTACK=>;
$string_bashisms{qr'(?:^|\s+)EUID='} = q<EUID=>;
$string_bashisms{qr'(?:^|\s+)UID='} = q<UID=>;
$string_bashisms{qr'(?:^|\s+)BASH(_[A-Z]+)?='} = q<BASH(_SOMETHING)=>;
$string_bashisms{qr'(?:^|\s+)SHELLOPTS='} = q<SHELLOPTS=>;
$string_bashisms{qr'\$\{?POSIXLY_CORRECT\}?\b'} = q<$POSIXLY_CORRECT>;
}
}

388
scripts/cowpoke.1 Normal file
View file

@ -0,0 +1,388 @@
.\" Hey, EMACS: -*- nroff -*-
.\" First parameter, NAME, should be all caps
.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
.\" other parameters are allowed: see man(7), man(1)
.TH COWPOKE 1 "April 28, 2008"
.\" Please adjust this date whenever revising the manpage.
.\"
.\" Some roff macros, for reference:
.\" .nh disable hyphenation
.\" .hy enable hyphenation
.\" .ad l left justify
.\" .ad b justify to both left and right margins
.\" .nf disable filling
.\" .fi enable filling
.\" .br insert line break
.\" .sp <n> insert n+1 empty lines
.\" for manpage-specific macros, see man(7)
.SH NAME
cowpoke \- Build a Debian source package in a remote cowbuilder instance
.SH SYNOPSIS
.B cowpoke
.RI [ options ] " packagename.dsc"
.SH DESCRIPTION
Uploads a Debian source package to a \fBcowbuilder\fR host and builds it,
optionally also signing and uploading the result to an incoming queue.
.SH OPTIONS
The following options are available:
.TP
.BI \-\-arch= architecture
Specify the Debian architecture(s) to build for. A space separated list of
architectures may be used to build for all of them in a single pass. Valid
arch names are those returned by \fBdpkg-architecture\fP(1) for
\fBDEB_BUILD_ARCH\fP.
.TP
.BI \-\-dist= distribution
Specify the Debian distribution(s) to build for. A space separated list of
distributions may be used to build for all of them in a single pass. Either
codenames (such as \fBsid\fP, or \fBsqueeze\fP) or distribution names (such as
\fBunstable\fP, or \fBexperimental\fP) may be used, but you should usually stick
to using one or the other consistently as this name may be used in file paths
and to locate old packages for comparison reporting.
It is now also possible to use locally defined names with this option, when
used in conjunction with the \fBBASE_DIST\fP option in a configuration file.
This permits the maintenance and use of specially configured build chroots,
which can source package dependencies from the backports archives or a local
repository, or have other unusual configuration options set, without polluting
the chroots you use for clean package builds intended for upload to the main
repositories. See the description of \fBBASE_DIST\fP below.
.TP
.BI \-\-buildd= host
Specify the remote host to build on.
.TP
.BI \-\-buildd\-user= name
Specify the remote user to build as.
.TP
.B \-\-create
Create the remote \fBcowbuilder\fR root if it does not already exist. If this option
is not passed it is an error for the specified \fB\-\-dist\fP or \fB\-\-arch\fP
to not have an existing \fBcowbuilder\fR root in the expected location.
The \fB\-\-buildd\-user\fP must have permission to create the \fBRESULT_DIR\fP
on the build host, or an admin with the necessary permission must first create
it and give that user (or some group they are in) write access to it, for this
option to succeed.
.TP
.BR \-\-return= [ \fIpath ]
Copy results of the build to \fIpath\fP. If \fIpath\fP is not specified, then return
them to the current directory. The given \fIpath\fP must exist, it will not be created.
.TP
.B \-\-no\-return
Do not copy results of the build to \fBRETURN_DIR\fP (overriding a path set for
it in the configuration files).
.TP
.BI \-\-dpkg\-opts= "'opt1 opt2 ...'"
Specify additional options to be passed to \fBdpkg-buildpackage\fP(1). Multiple
options are delimited with spaces. This will override any options specified in
\fBDEBBUILDOPTS\fP in the build host's \fIpbuilderrc\fP.
.TP
.BI \-\-create\-opts= "'cowbuilder option'"
Specify additional arguments to be passed verbatim to \fBcowbuilder\fR when a
chroot is first created (using the \fB\-\-create\fP option above). If multiple
arguments need to be passed, this option should be specified separately for
each of them.
E.g., \fB\-\-create\-opts "\-\-othermirror" \-\-create\-opts "deb http:// ..."\fP
This option will override any \fBCREATE_OPTS\fP specified for a chroot in the
cowpoke configuration files.
.TP
.BI \-\-update\-opts= "'cowbuilder option'"
Specify additional arguments to be passed verbatim to \fBcowbuilder\fR if the
base of the chroot is updated. If multiple arguments need to be passed, this
option should be specified separately for each of them.
This option will override any \fBUPDATE_OPTS\fP specified for a chroot in the
cowpoke configuration files.
.TP
.BI \-\-build\-opts= "'cowbuilder option'"
Specify additional arguments to be passed verbatim to \fBcowbuilder\fR when
a package build is performed. If multiple arguments need to be passed, this
option should be specified separately for each of them.
This option will override any \fBBUILD_OPTS\fP specified for a chroot in the
cowpoke configuration files.
.TP
.BI \-\-sign= keyid
Specify the key to sign packages with. This will override any \fBSIGN_KEYID\fP
specified for a chroot in the cowpoke configuration files.
.TP
.BI \-\-upload= queue
Specify the dput queue to upload signed packages to. This will override any
\fBUPLOAD_QUEUE\fP specified for a chroot in the cowpoke configuration files.
.TP
.B \-\-help
Display a brief summary of the available options and current configuration.
.TP
.B \-\-version
Display the current version information.
.SH CONFIGURATION OPTIONS
When \fBcowpoke\fP is run the following configuration options are read from
global, per\-user, and per\-project configuration files if present. File paths
may be absolute or relative, the latter being relative to the \fBBUILDD_USER\fR's
home directory. Since the paths are typically quoted when used, tilde expansion
will \fBnot\fP be performed on them.
.SS Global defaults
These apply to every \fIarch\fP and \fIdist\fP in a single cowpoke invocation.
.TP
.B BUILDD_HOST
The network address or fqdn of the build machine where \fBcowbuilder\fR is configured.
This may be overridden by the \fB\-\-buildd\fP command line option.
.TP
.B BUILDD_USER
The unprivileged user name for operations on the build machine. This defaults
to the local name of the user executing \fBcowpoke\fP (or to a username that is
specified in your SSH configuration for \fBBUILDD_HOST\fR), and may be overridden by the
\fB\-\-buildd\-user\fP command line option.
.TP
.B BUILDD_ARCH
The Debian architecture(s) to build for. This must match the \fBDEB_BUILD_ARCH\fP
of the build chroot being used. It defaults to the local machine architecture where
\fBcowpoke\fP is executed, and may be overridden by the \fB\-\-arch\fP command line
option. A (quoted) space separated list of architectures may be used here to build
for all of them in a single pass.
.TP
.B BUILDD_DIST
The Debian distribution(s) to build for. A (quoted) space separated list of
distributions may be used to build for all of them in a single pass. This may
be overridden by the \fB\-\-dist\fP command line option.
.TP
.B INCOMING_DIR
The directory path on the build machine where the source package will initially
be placed. This must be writable by the \fBBUILDD_USER\fP.
.TP
.B PBUILDER_BASE
The filesystem root for all pbuilder CoW and result files. \fIArch\fP and \fIdist\fP
specific subdirectories will normally be created under this. The apt cache
and temporary build directory will also be located under this path.
.TP
.B SIGN_KEYID
If this option is set, it is expected to contain the OpenPGP key ID to pass to
\fBdebsign\fP(1) if the packages are to be remotely signed. You will be prompted
to confirm whether you wish to sign the packages after all builds are complete.
If this option is unset or an empty string, no attempt to sign packages will be
made. It may be overridden on an \fIarch\fP and \fIdist\fP specific basis using
the
.IB arch _ dist _SIGN_KEYID
option described below, or per-invocation with the \fB\-\-sign\fP command line
option.
.TP
.B UPLOAD_QUEUE
If this option is set, it is expected to contain a 'host' specification for
\fBdput\fP(1) which will be used to upload them after they are signed. You will
be prompted to confirm whether you wish to upload the packages after they are
signed. If this option is unset or an empty string, no attempt to upload packages
will be made. If \fBSIGN_KEYID\fP is not set, this option will be ignored entirely.
It may be overridden on an \fIarch\fP and \fIdist\fP specific basis using the
.IB arch _ dist _UPLOAD_QUEUE
option described below, or per-invocation with the \fB\-\-upload\fP command line
option.
.TP
.B BUILDD_ROOTCMD
The command to use to gain root privileges on the remote build machine. If
unset the default is \fBsudo\fP(8). This is only required to invoke \fBcowbuilder\fR
and allow it to enter its chroot, so you may restrict this user to only being
able to run that command with escalated privileges. Something like this in
sudoers will enable invoking \fBcowbuilder\fR without an additional password entry
required:
.TP
.B " "
.RS 1.5i
youruser ALL = NOPASSWD: /usr/sbin/cowbuilder
.RE
.TP
.B " "
Alternatively you could use SSH with a forwarded key, or whatever other
mechanism suits your local access policy. Using \fBsu \-c\fR isn't really
suitable here due to its quoting requirements being somewhat different to
the rest.
.TP
.B DEBOOTSTRAP
The utility to use when creating a new build root. Alternatives are
.BR debootstrap " or " cdebootstrap .
.TP
.B RETURN_DIR
If set, package files resulting from the build will be copied to the path
(local or remote) that this is set to, after the build completes. The path
must exist, it will not be created. This option is unset by default and can
be overridden with \fB\-\-return\fR or \fB\-\-no-return\fR.
.SS Arch and dist specific options
These are variables of the form: $arch_$dist\fB_VAR\fR which apply only for a
particular target arch/dist build.
.TP
.IB arch _ dist _RESULT_DIR
The directory path on the build machine where the resulting packages (source and
binary) will be found, and where older versions of the package that were built
previously may be found. If any such older packages exist, \fBdebdiff\fP will
be used to compare the new package with the previous version after the build is
complete, and the result will be included in the build log. Files in it must be
readable by the \fBBUILDD_USER\fP for sanity checking with \fBlintian\fP(1) and
\fBdebdiff\fP(1), and for upload with \fBdput\fP(1). If this option is not
specified for some arch and dist combination then it will default to
.I $PBUILDER_BASE/$arch/$dist/result
.TP
.IB arch _ dist _BASE_PATH
The directory where the CoW master files are to be found (or created if the
\fB\-\-create\fP command line option was passed). If this option is not specified
for some arch or dist then it will default to
.I $PBUILDER_BASE/$arch/$dist/base.cow
.TP
.IB arch _ dist _BASE_DIST
The code name to pass as the \fB\-\-distribution\fP option for cowbuilder instead
of \fIdist\fP. This is necessary when \fIdist\fP is a locally significant name
assigned to some specially configured build chroot, such as 'wheezy_backports',
and not the formal suite name of a distro release known to debootstrap. This
option cannot be overridden on the command line, since it would rarely, if ever,
make any sense to change it for individual invocations of \fBcowpoke\fP. If this
option is not specified for an arch and dist combination then it will default to
.IR dist .
.TP
.IB arch _ dist _CREATE_OPTS
A bash array containing additional options to pass verbatim to \fBcowbuilder\fP
when this chroot is created for the first time (using the \fB\-\-create\fP option).
This is useful when options like \fB\-\-othermirror\fP are wanted to create
specialised chroot configurations such as 'wheezy_backports'. By default this
is unset. All values set in it will be overridden if the \fB\-\-create\-opts\fP
option is passed on the command line.
Each element in this array corresponds to a single argument (in the ARGV sense)
that will be passed to cowbuilder. This ensures that arguments which may contain
whitespace or have strange quoting requirements or other special characters will
not be mangled before they get to cowbuilder.
Bash arrays are initialised using the following form:
OPTS=( "arg1" "arg 2" "\-\-option" "value" "\-\-opt=val" "etc. etc." )
.TP
.IB arch _ dist _UPDATE_OPTS
A bash array containing additional options to pass verbatim to \fBcowbuilder\fP
each time the base of this chroot is updated. It behaves similarly to the
\fBCREATE_OPTS\fP option above, except for acting when the chroot is updated.
.TP
.IB arch _ dist _BUILD_OPTS
A bash array containing additional options to pass verbatim to \fBcowbuilder\fP
each time a package build is performed in this chroot. This is useful when you
want to use some option like \fB\-\-twice\fP which cowpoke does not directly
need to care about. It otherwise behaves similarly to \fBUPDATE_OPTS\fP above
except that it acts during the build phase of \fBcowbuilder\fP.
.TP
.IB arch _ dist _SIGN_KEYID
An optional arch and dist specific override for the global \fBSIGN_KEYID\fP
option.
.TP
.IB arch _ dist _UPLOAD_QUEUE
An optional arch and dist specific override for the global \fBUPLOAD_QUEUE\fP
option.
.SH CONFIGURATION FILES
.TP
.I /etc/cowpoke.conf
Global configuration options. Will override hardcoded defaults.
.TP
.I ~/.cowpoke
Per\-user configuration options. Will override any global configuration.
.TP
.I .cowpoke
Per\-project configuration options. Will override any per-user or global
configuration if \fBcowpoke\fP is called from the directory where they exist.
If the environment variable \fBCOWPOKE_CONF\fP is set, it specifies an additional
configuration file which will override all of those above. Options specified
explicitly on the command line override all configuration files.
.SH COWBUILDER CONFIGURATION
There is nothing particularly special required to configure a \fBcowbuilder\fR instance
for use with \fBcowpoke\fP. Simply create them in the flavour you require with
`\fBcowbuilder \-\-create\fP` according to the \fBcowbuilder\fR documentation, then
configure \fBcowpoke\fP with the user, arch, and path information required to
access it, on the machines you wish to invoke it from (or alternatively configure
\fBcowpoke\fP with the path, arch and distribution information and pass the
\fB\-\-create\fP option to it on the first invocation). The build host running
\fBcowbuilder\fR does not require \fBcowpoke\fP installed locally.
The build machine should have the \fBlintian\fP and \fBdevscripts\fR packages
installed for post-build sanity checking. Upon completion, the build log and
the results of automated checks will be recorded in the \fBINCOMING_DIR\fP.
If you wish to upload signed packages the build machine will also need
\fBdput\fP(1) installed and configured to use the '\fIhost\fP' alias specified
by \fBUPLOAD_QUEUE\fP. If \fBrsync\fP(1) is available on both the local and
build machine, then it will be used to transfer the source package (this may
save on some transfers of the \fIorig.tar.*\fP when building subsequent Debian
revisions).
The user executing \fBcowpoke\fP must have SSH access to the build machine as
the \fBBUILDD_USER\fP. That user must be able to invoke \fBcowbuilder\fR as root by
using the \fBBUILDD_ROOTCMD\fP. Signing keys are not required to be installed
on the build machine (and will be ignored there if they are). If the package
is signed, keys will be expected on the machine that executes \fBcowpoke\fP.
When \fBcowpoke\fP is invoked, it will first attempt to update the \fBcowbuilder\fR
image if that has not already been done on the same day. This is checked by
the presence or absence of a \fIcowbuilder-$arch-$dist-update-log-$date\fP file
in the \fBINCOMING_DIR\fP. You may move, remove, or touch this file if you wish
the image to be updated more or less often than that. Its contents log the
output of \fBcowbuilder\fR during the update (or creation) of the build root.
.SH NOTES
Since \fBcowbuilder\fP creates a chroot, and to do that you need root, \fBcowpoke\fP
also requires some degree of root access. So all the horrible things that can
go wrong with that may well one day rain down upon you. \fBcowbuilder\fR has been
known to accidentally wipe out bind-mounted filesystems outside the chroot, and
worse than that can easily happen. So be careful, keep good backups of things
you don't want to lose on your build machine, and use \fBcowpoke\fP to keep all
that on a machine that isn't your bleeding edge dev box with your last few hours
of uncommitted work.
.SH SEE ALSO
.BR cowbuilder (1),
.BR pbuilder (1),
.BR ssh-agent (1),
.BR sudoers (5)
.SH AUTHOR
.B cowpoke
was written by Ron <\fIron@debian.org\fP>.

542
scripts/cowpoke.sh Executable file
View file

@ -0,0 +1,542 @@
#!/bin/bash
# Simple shell script for driving a remote cowbuilder via ssh
#
# Copyright(C) 2007, 2008, 2009, 2011, 2012, 2014, Ron <ron@debian.org>
# This script is distributed according to the terms of the GNU GPL.
set -e
#BUILDD_HOST=
#BUILDD_USER=
BUILDD_ARCH="$(dpkg-architecture -qDEB_BUILD_ARCH 2>/dev/null)"
# The 'default' dist is whatever cowbuilder is locally configured for
BUILDD_DIST="default"
INCOMING_DIR="cowbuilder-incoming"
PBUILDER_BASE="/var/cache/pbuilder"
#SIGN_KEYID=
#UPLOAD_QUEUE="ftp-master"
BUILDD_ROOTCMD="sudo"
REMOTE_SCRIPT="cowssh_it"
DEBOOTSTRAP="cdebootstrap"
for f in /etc/cowpoke.conf ~/.cowpoke .cowpoke "$COWPOKE_CONF"; do [ -r "$f" ] && . "$f"; done
get_archdist_vars() {
_ARCHDIST_OPTIONS="RESULT_DIR BASE_PATH BASE_DIST CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"
_RESULT_DIR="result"
_BASE_PATH="base.cow"
for arch in $BUILDD_ARCH; do
for dist in $BUILDD_DIST; do
for var in $_ARCHDIST_OPTIONS; do
eval "val=( \"\${${arch}_${dist}_${var}[@]}\" )"
if [ "$1" = "display" ]; then
case $var in
RESULT_DIR | BASE_PATH )
[ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
echo " ${arch}_${dist}_${var} = $val"
;;
*_OPTS )
# Don't display these if they are overridden on the command line.
eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
[ ${#override[@]} -gt 0 ] || [ ${#val[@]} -eq 0 ] ||
echo " ${arch}_${dist}_${var} =$(printf " '%s'" "${val[@]}")"
;;
* )
[ ${#val[@]} -eq 0 ] || echo " ${arch}_${dist}_${var} = $val"
;;
esac
else
case $var in
RESULT_DIR | BASE_PATH )
# These are always a single value, and must always be set,
# either by the user or to their default value.
[ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
echo "${arch}_${dist}_${var}='$val'"
;;
*_OPTS )
# These may have zero, one, or many values which we must not word-split.
# They can safely remain unset if there are no values.
#
# We don't need to worry about the command line overrides here,
# they will be taken care of in the remote script.
[ ${#val[@]} -eq 0 ] ||
echo "${arch}_${dist}_${var}=($(printf " %q" "${val[@]}") )"
;;
SIGN_KEYID | UPLOAD_QUEUE )
# We don't need these in the remote script
;;
* )
# These may have zero or one value.
# They can safely remain unset if there are no values.
[ ${#val[@]} -eq 0 ] || echo "${arch}_${dist}_${var}='$val'"
;;
esac
fi
done
done
done
}
display_override_vars() {
_OVERRIDE_OPTIONS="CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"
for var in $_OVERRIDE_OPTIONS; do
eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
[ ${#override[@]} -eq 0 ] || echo " override: $var =$(printf " '%s'" "${override[@]}")"
done
}
PROGNAME=${0##*/}
version() {
echo \
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
This code is Copyright 2007-2014, Ron <ron@debian.org>.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License."
exit 0
}
usage() {
cat 1>&2 <<EOF
cowpoke [options] package.dsc
Uploads a Debian source package to a cowbuilder host and builds it,
optionally also signing and uploading the result to an incoming queue.
The following options are supported:
--arch="arch" Specify the Debian architecture(s) to build for.
--dist="dist" Specify the Debian distribution(s) to build for.
--buildd="host" Specify the remote host to build on.
--buildd-user="name" Specify the remote user to build as.
--create Create the remote cowbuilder root if necessary.
--return[="path"] Copy results of the build to 'path'. If path is
not specified, return them to the current directory.
--no-return Do not copy results of the build to RETURN_DIR
(overriding a path set for it in the config files).
--sign="keyid" Specify the key to sign packages with.
--upload="queue" Specify the dput queue to upload signed packages to.
The current default configuration is:
BUILDD_HOST = $BUILDD_HOST
BUILDD_USER = $BUILDD_USER
BUILDD_ARCH = $BUILDD_ARCH
BUILDD_DIST = $BUILDD_DIST
RETURN_DIR = $RETURN_DIR
SIGN_KEYID = $SIGN_KEYID
UPLOAD_QUEUE = $UPLOAD_QUEUE
The expected remote paths are:
INCOMING_DIR = $INCOMING_DIR
PBUILDER_BASE = ${PBUILDER_BASE:-/}
$(get_archdist_vars display)
$(display_override_vars)
The cowbuilder image must have already been created on the build host
and the expected remote paths must already exist if the --create option
is not passed. You must have ssh access to the build host as BUILDD_USER
if that is set, else as the user executing cowpoke or a user specified
in your ssh config for '$BUILDD_HOST'.
That user must be able to execute cowbuilder as root using '$BUILDD_ROOTCMD'.
EOF
exit $1
}
for arg; do
case "$arg" in
--arch=*)
BUILDD_ARCH="${arg#*=}"
;;
--dist=*)
BUILDD_DIST="${arg#*=}"
;;
--buildd=*)
BUILDD_HOST="${arg#*=}"
;;
--buildd-user=*)
BUILDD_USER="${arg#*=}"
;;
--create)
CREATE_COW="yes"
;;
--return=*)
RETURN_DIR="${arg#*=}"
;;
--return)
RETURN_DIR=.
;;
--no-return)
RETURN_DIR=
;;
--dpkg-opts=*)
# This one is a bit tricky, given the combination of the calling convention here,
# the calling convention for cowbuilder, and the behaviour of things that might
# pass this option to us. Some things, like when we are called from the gitpkg
# hook using options from git-config, will preserve any quoting that was used in
# the .gitconfig file, which is natural for anyone to want to use in a construct
# like: options = --dpkg-opts='-uc -us -j6'. People are going to cringe if we
# tell them they must not use quotes there no matter how much it may 'make sense'
# if you know too much about the internals. And it will only get worse when we
# then tell them they must quote it like that if they type it directly in their
# shell ...
#
# So we do the only thing that seems sensible, and try to Deal With It here.
# If the outermost characters are paired quotes, we manually strip them off.
# We don't want to let the shell do quote removal, since that might change a
# part of this which we don't want modified.
# We collect however many sets of those we are passed in an array, which we'll
# then combine back into a single argument at the final point of use.
#
# Which _should_ DTRT for anyone who isn't trying to blow this up deliberately
# and maybe will still do it for them too in spite of their efforts. But unless
# someone finds a sensible case this fails on, I'm not going to cry over people
# who want to stuff up their own system with input they created themselves.
val=${arg#*=}
[[ $val == \'*\' || $val == \"*\" ]] && val=${val:1:-1}
DEBBUILDOPTS+=( "$val" )
;;
--create-opts=*)
OVERRIDE_CREATE_OPTS+=( "${arg#*=}" )
;;
--update-opts=*)
OVERRIDE_UPDATE_OPTS+=( "${arg#*=}" )
;;
--build-opts=*)
OVERRIDE_BUILD_OPTS+=( "${arg#*=}" )
;;
--sign=*)
OVERRIDE_SIGN_KEYID=${arg#*=}
;;
--upload=*)
OVERRIDE_UPLOAD_QUEUE=${arg#*=}
;;
*.dsc)
DSC="$arg"
;;
--help)
usage 0
;;
--version)
version
;;
*)
echo "ERROR: unrecognised option '$arg'"
usage 1
;;
esac
done
if [ -z "$REMOTE_SCRIPT" ]; then
echo "No remote script name set. Aborted."
exit 1
fi
if [ -z "$DSC" ]; then
echo "ERROR: No package .dsc specified"
usage 1
fi
if ! [ -r "$DSC" ]; then
echo "ERROR: '$DSC' not found."
exit 1
fi
if [ -z "$BUILDD_ARCH" ]; then
echo "No BUILDD_ARCH set. Aborted."
exit 1
fi
if [ -z "$BUILDD_HOST" ]; then
echo "No BUILDD_HOST set. Aborted."
exit 1
fi
if [ -z "$BUILDD_ROOTCMD" ]; then
echo "No BUILDD_ROOTCMD set. Aborted."
exit 1
fi
if [ -e "$REMOTE_SCRIPT" ]; then
echo "$REMOTE_SCRIPT file already exists and will be overwritten."
echo -n "Do you wish to continue (Y/n)? "
read -e yesno
case "$yesno" in
N* | n*)
echo "Ok, bailing out."
echo "You should set the REMOTE_SCRIPT variable to some other value"
echo "if this name conflicts with something you already expect to use"
exit 1
;;
*) ;;
esac
fi
[ -z "$BUILDD_USER" ] || BUILDD_USER="$BUILDD_USER@"
PACKAGE="$(basename $DSC .dsc)"
DATE="$(date +%Y%m%d 2>/dev/null)"
cat > "$REMOTE_SCRIPT" <<-EOF
#!/bin/bash
# cowpoke generated remote worker script.
# Normally this should have been deleted already, you can safely remove it now.
compare_changes() {
p1="\${1%_*.changes}"
p2="\${2%_*.changes}"
p1="\${p1##*_}"
p2="\${p2##*_}"
dpkg --compare-versions "\$p1" gt "\$p2"
}
$(get_archdist_vars)
for arch in $BUILDD_ARCH; do
for dist in $BUILDD_DIST; do
echo " ------- Begin build for \$arch \$dist -------"
CHANGES="\$arch.changes"
LOGFILE="$INCOMING_DIR/build.${PACKAGE}_\$arch.\$dist.log"
UPDATELOG="$INCOMING_DIR/cowbuilder-\${arch}-\${dist}-update-log-$DATE"
eval "RESULT_DIR=\"\\\$\${arch}_\${dist}_RESULT_DIR\""
eval "BASE_PATH=\"\\\$\${arch}_\${dist}_BASE_PATH\""
eval "BASE_DIST=\"\\\$\${arch}_\${dist}_BASE_DIST\""
eval "CREATE_OPTS=( \"\\\${\${arch}_\${dist}_CREATE_OPTS[@]}\" )"
eval "UPDATE_OPTS=( \"\\\${\${arch}_\${dist}_UPDATE_OPTS[@]}\" )"
eval "BUILD_OPTS=( \"\\\${\${arch}_\${dist}_BUILD_OPTS[@]}\" )"
[ -n "\$BASE_DIST" ] || BASE_DIST=\$dist
[ ${#OVERRIDE_CREATE_OPTS[@]} -eq 0 ] || CREATE_OPTS=("${OVERRIDE_CREATE_OPTS[@]}")
[ ${#OVERRIDE_UPDATE_OPTS[@]} -eq 0 ] || UPDATE_OPTS=("${OVERRIDE_UPDATE_OPTS[@]}")
[ ${#OVERRIDE_BUILD_OPTS[@]} -eq 0 ] || BUILD_OPTS=("${OVERRIDE_BUILD_OPTS[@]}")
[ ${#DEBBUILDOPTS[*]} -eq 0 ] || DEBBUILDOPTS=("--debbuildopts" "${DEBBUILDOPTS[*]}")
# Sort the list of old changes files for this package to try and
# determine the most recent one preceding this version. We will
# debdiff to this revision in the final sanity checks if one exists.
# This is adapted from the insertion sort trickery in git-debimport.
OLD_CHANGES="\$(find "\$RESULT_DIR/" -maxdepth 1 -type f \\
-name "${PACKAGE%%_*}_*_\$CHANGES" 2>/dev/null \\
| sort 2>/dev/null)"
P=( \$OLD_CHANGES )
count=\${#P[*]}
for(( i=1; i < count; ++i )) do
j=i
#echo "was \$i: \${P[i]}"
while ((\$j)) && compare_changes "\${P[j-1]}" "\${P[i]}"; do ((--j)); done
((i==j)) || P=( \${P[@]:0:j} \${P[i]} \${P[j]} \${P[@]:j+1:i-(j+1)} \${P[@]:i+1} )
done
#for(( i=1; i < count; ++i )) do echo "now \$i: \${P[i]}"; done
OLD_CHANGES=
for(( i=count-1; i >= 0; --i )) do
if [ "\${P[i]}" != "\$RESULT_DIR/${PACKAGE}_\$CHANGES" ]; then
OLD_CHANGES="\${P[i]}"
break
fi
done
set -eo pipefail
if ! [ -e "\$BASE_PATH" ]; then
if [ "$CREATE_COW" = "yes" ]; then
mkdir -p "\$RESULT_DIR"
mkdir -p "\$(dirname \$BASE_PATH)"
mkdir -p "$PBUILDER_BASE/aptcache"
$BUILDD_ROOTCMD cowbuilder --create --distribution \$BASE_DIST \\
--basepath "\$BASE_PATH" \\
--aptcache "$PBUILDER_BASE/aptcache" \\
--debootstrap "$DEBOOTSTRAP" \\
--debootstrapopts --arch="\$arch" \\
"\${CREATE_OPTS[@]}" \\
2>&1 | tee "\$UPDATELOG"
else
echo "SKIPPING \$dist/\$arch build, '\$BASE_PATH' does not exist" | tee "\$LOGFILE"
echo " use the cowpoke --create option to bootstrap a new build root" | tee -a "\$LOGFILE"
continue
fi
elif ! [ -e "\$UPDATELOG" ]; then
$BUILDD_ROOTCMD cowbuilder --update --distribution \$BASE_DIST \\
--basepath "\$BASE_PATH" \\
--aptcache "$PBUILDER_BASE/aptcache" \\
--autocleanaptcache \\
"\${UPDATE_OPTS[@]}" \\
2>&1 | tee "\$UPDATELOG"
fi
$BUILDD_ROOTCMD cowbuilder --build --basepath "\$BASE_PATH" \\
--aptcache "$PBUILDER_BASE/aptcache" \\
--buildplace "$PBUILDER_BASE/build" \\
--buildresult "\$RESULT_DIR" \\
"\${DEBBUILDOPTS[@]}" \\
"\${BUILD_OPTS[@]}" \\
"$INCOMING_DIR/$(basename $DSC)" 2>&1 \\
| tee "\$LOGFILE"
set +eo pipefail
echo >> "\$LOGFILE"
echo "lintian \$RESULT_DIR/${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
lintian "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 | tee -a "\$LOGFILE"
if [ -n "\$OLD_CHANGES" ]; then
echo >> "\$LOGFILE"
echo "debdiff \$OLD_CHANGES ${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
debdiff "\$OLD_CHANGES" "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 \\
| tee -a "\$LOGFILE"
else
echo >> "\$LOGFILE"
echo "No previous packages for \$dist/\$arch to compare" >> "\$LOGFILE"
fi
done
done
EOF
chmod 755 "$REMOTE_SCRIPT"
if ! dcmd rsync -vP $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR";
then
dcmd scp $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR"
fi
ssh -t "$BUILDD_USER$BUILDD_HOST" "\"$INCOMING_DIR/$REMOTE_SCRIPT\" && rm -f \"$INCOMING_DIR/$REMOTE_SCRIPT\""
echo
echo "Build completed."
for arch in $BUILDD_ARCH; do
CHANGES="$arch.changes"
for dist in $BUILDD_DIST; do
sign_keyid=$OVERRIDE_SIGN_KEYID
[ -n "$sign_keyid" ] || eval "sign_keyid=\"\$${arch}_${dist}_SIGN_KEYID\""
[ -n "$sign_keyid" ] || sign_keyid="$SIGN_KEYID"
[ -n "$sign_keyid" ] || continue
eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"
_desc="$dist/$arch"
[ "$dist" != "default" ] || _desc="$arch"
while true; do
echo -n "Sign $_desc $PACKAGE with key '$sign_keyid' (yes/no)? "
read -e yesno
case "$yesno" in
YES | yes)
debsign "-k$sign_keyid" -r "$BUILDD_USER$BUILDD_HOST" "$RESULT_DIR/${PACKAGE}_$CHANGES"
upload_queue=$OVERRIDE_UPLOAD_QUEUE
[ -n "$upload_queue" ] || eval "upload_queue=\"\$${arch}_${dist}_UPLOAD_QUEUE\""
[ -n "$upload_queue" ] || upload_queue="$UPLOAD_QUEUE"
if [ -n "$upload_queue" ]; then
while true; do
echo -n "Upload $_desc $PACKAGE to '$upload_queue' (yes/no)? "
read -e upload
case "$upload" in
YES | yes)
ssh "$BUILDD_USER$BUILDD_HOST" \
"cd \"$RESULT_DIR/\" && dput \"$upload_queue\" \"${PACKAGE}_$CHANGES\""
break 2
;;
NO | no)
echo "Package upload skipped."
break 2
;;
*)
echo "Please answer 'yes' or 'no'"
;;
esac
done
fi
break
;;
NO | no)
echo "Package signing skipped."
break
;;
*)
echo "Please answer 'yes' or 'no'"
;;
esac
done
done
done
if [ -n "$RETURN_DIR" ]; then
for arch in $BUILDD_ARCH; do
CHANGES="$arch.changes"
for dist in $BUILDD_DIST; do
eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"
cache_dir="./cowpoke-return-cache"
mkdir -p $cache_dir
scp "$BUILDD_USER$BUILDD_HOST:$RESULT_DIR/${PACKAGE}_$CHANGES" $cache_dir
for f in $(cd $cache_dir && dcmd ${PACKAGE}_$CHANGES); do
RESULTS="$RESULTS $RESULT_DIR/$f"
done
rm -f $cache_dir/${PACKAGE}_$CHANGES
rmdir $cache_dir
if ! rsync -vP "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR" ;
then
scp "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR"
fi
done
done
fi
rm -f "$REMOTE_SCRIPT"
# vi:sts=4:sw=4:noet:foldmethod=marker

115
scripts/dcmd.1 Normal file
View file

@ -0,0 +1,115 @@
.TH DCMD 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
dcmd \- expand file lists of .dsc/.changes files in the command line
.SH SYNOPSIS
\fBdcmd\fR [\fIoptions\fR] [\fIcommand\fR] [\fIchanges-file\fR|\fIdsc-file\fR] ...
.SH DESCRIPTION
\fBdcmd\fR replaces any reference to a \fI.dsc\fR or \fI.changes\fR file in the
command line with the list of files in its 'Files' section, plus the
file itself. It allows easy manipulation of all the files involved in
an upload (for \fI.changes\fR files) or a source package (for \fI.dsc\fR files).
If \fIcommand\fR is omitted (that is the first argument is an existing \fI.dsc\fR
or \fI.changes\fR file), the expanded list of files is printed to stdout, one file
by line. Useful for usage in backticks.
.SH OPTIONS
There are a number of options which may be used in order to select only a
subset of the files listed in the \fI.dsc\fR or \fI.changes\fR file. If a requested file
is not found, an error message will be printed.
.TP 14
.B \-\-dsc
Select the \fI.dsc\fR file.
.TP
.B \-\-buildinfo
Select the \fI.buildinfo\fR file.
.TP
.B \-\-schanges
Select \fI.changes\fR files for the 'source' architecture.
.TP
.B \-\-bchanges
Select \fI.changes\fR files for binary architectures.
.TP
.B \-\-changes
Select \fI.changes\fR files. Implies \fB\-\-schanges\fR and \fB\-\-bchanges\fR.
.TP
.B \-\-archdeb
Select architecture-dependent binary packages (\fI.deb\fR files).
.TP
.B \-\-indepdeb
Select architecture-independent binary packages (\fI.deb\fR files).
.TP
.B \-\-deb
Select binary packages (\fI.deb\fR files). Implies \fB\-\-archdeb\fR and \fB\-\-indepdeb\fR.
.TP
.B \-\-archudeb
Select architecture-dependent \fI.udeb\fR binary packages.
.TP
.B \-\-indepudeb
Select architecture-independent \fI.udeb\fR binary packages.
.TP
.B \-\-udeb
Select \fI.udeb\fR binary packages. Implies \fB\-\-archudeb\fR and \fB\-\-indepudeb\fR.
.TP
.BR \-\-tar ,\ \-\-orig
Select the upstream \fI.tar\fR file.
.TP
.BR \-\-diff ,\ \-\-debtar
Select the Debian \fI.debian.tar\fR or \fI.diff\fR file.
.PP
Each option may be prefixed by \fB\-\-no\fR to indicate that all files
\fInot\fR matching the specification should be selected.
.PP
It is not possible to combine positive filtering options (e.g. \fB\-\-dsc\fR)
and negative filtering options (e.g. \fB\-\-no\-changes\fR) in the same
\fBdcmd\fR invocation.
.TP
.B \-\-no\-fail\-on\-missing\fR, \fB\-r
If any of the requested files were not found, do not output an error.
.TP
.B \-\-package\fR, \fB\-p
Output package name part only.
.TP
.B \-\-sort\fR, \fB\-s
Sort output alphabetically.
.TP
.B \-\-tac\fR, \fB\-t
Reverse output order.
.SH "EXAMPLES"
Copy the result of a build to another machine:
.nf
$ dcmd scp rcs_5.7-23_amd64.changes elegiac:/tmp
rcs_5.7-23.dsc 100% 490 0.5KB/s 00:00
rcs_5.7-23.diff.gz 100% 12KB 11.7KB/s 00:00
rcs_5.7-23_amd64.deb 100% 363KB 362.7KB/s 00:00
rcs_5.7-23_amd64.changes 100% 1095 1.1KB/s 00:00
$
$ dcmd \-\-diff \-\-deb scp rcs_5.7-23_amd64.changes elegiac:/tmp
rcs_5.7-23.diff.gz 100% 12KB 11.7KB/s 00:00
rcs_5.7-23_amd64.deb 100% 363KB 362.7KB/s 00:00
$
.fi
Check the contents of a source package:
.nf
$ dcmd md5sum rcs_5.7-23.dsc
8fd09ea9654cda128f8d5c337d3b8de7 rcs_5.7.orig.tar.gz
f0ceeae96603e823eacba6721a30b5c7 rcs_5.7-23.diff.gz
5241db1e231b1f43ae5514b63d2523f8 rcs_5.7-23.dsc
$
$ dcmd \-\-no\-diff md5sum rcs_5.7-23.dsc
8fd09ea9654cda128f8d5c337d3b8de7 rcs_5.7.orig.tar.gz
5241db1e231b1f43ae5514b63d2523f8 rcs_5.7-23.dsc
$
.fi
.SH "SEE ALSO"
.BR dpkg-genchanges (1),
.BR dpkg-source (1)
.SH AUTHOR
This program was written by Romain Francoise <rfrancoise@debian.org> and
is released under the GPL, version 2 or later.

326
scripts/dcmd.sh Executable file
View file

@ -0,0 +1,326 @@
#!/bin/sh
#
# dcmd: expand file lists of .dsc/.changes files in the command line
#
# Copyright (C) 2008 Romain Francoise <rfrancoise@debian.org>
# Copyright (C) 2008 Christoph Berg <myon@debian.org>
# Copyright (C) 2008 Adam D. Barratt <adsb@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# Usage:
#
# dcmd replaces any reference to a .dsc or .changes file in the command
# line with the list of files in its 'Files' section, plus the
# .dsc/.changes file itself.
#
# $ dcmd sha1sum rcs_5.7-23_amd64.changes
# f61254e2b61e483c0de2fc163321399bbbeb43f1 rcs_5.7-23.dsc
# 7a2b283b4c505d8272a756b230486a9232376771 rcs_5.7-23.diff.gz
# e3bac970a57a6b0b41c28c615f2919c931a6cb68 rcs_5.7-23_amd64.deb
# c531310b18773d943249cfaa8b539a9b6e14b8f4 rcs_5.7-23_amd64.changes
# $
PROGNAME=${0##*/}
version() {
echo \
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
This code is copyright 2008 by Romain Francoise, all rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later."
}
usage() {
printf "Usage: %s [options] [command] [dsc or changes file] [...]\n" $PROGNAME
}
endswith() {
case $1 in
*$2) return 0 ;;
*) return 1;;
esac
}
# Instead of parsing the file completely as the previous Python
# implementation did (using python-debian), let's just select lines
# that look like they might be part of the file list.
RE="^ [0-9a-f]{32} [0-9]+ ((([a-zA-Z0-9_.-]+/)?[a-zA-Z0-9_.-]+|-) ([a-zA-Z]+|-) )?(.*)$"
maybe_expand() {
local dir
local sedre
if [ -e "$1" ] && (endswith "$1" .changes || endswith "$1" .dsc || endswith "$1" .buildinfo); then
# Need to escape whatever separator is being used in sed expression so
# it doesn't prematurely end the s command
dir=$(dirname "$1" | sed 's/,/\\,/g')
if [ "$(echo "$1" | cut -b1-2)" != "./" ]; then
sedre="\."
fi
sed --regexp-extended -n "s,$RE,$dir/\5,p" <"$1" | sed "s,^$sedre/,,"
fi
}
DSC=1; BCHANGES=1; SCHANGES=1; ARCHDEB=1; INDEPDEB=1; TARBALL=1; DIFF=1
CHANGES=1; DEB=1; ARCHUDEB=1; INDEPUDEB=1; UDEB=1; BUILDINFO=1;
FILTERED=0; FAIL_MISSING=1
EXTRACT_PACKAGE_NAME=0
SORT=0
TAC=0
while [ $# -gt 0 ]; do
TYPE=""
case "$1" in
--version|-v) version; exit 0;;
--help|-h) usage; exit 0;;
--no-fail-on-missing|-r) FAIL_MISSING=0;;
--fail-on-missing) FAIL_MISSING=1;;
--package|-p) EXTRACT_PACKAGE_NAME=1;;
--sort|-s) SORT=1;;
--tac|-t) TAC=1;;
--) shift; break;;
--no-*)
TYPE=${1#--no-}
case "$FILTERED" in
1) echo "$PROGNAME: Can't combine --foo and --no-foo options" >&2;
exit 1;;
0) FILTERED=-1;;
esac;;
--**)
TYPE=${1#--}
case "$FILTERED" in
-1) echo "$PROGNAME: Can't combine --foo and --no-foo options" >&2;
exit 1;;
0) FILTERED=1; DSC=0; BCHANGES=0; SCHANGES=0; CHANGES=0
ARCHDEB=0; INDEPDEB=0; DEB=0; ARCHUDEB=0; INDEPUDEB=0
UDEB=0; TARBALL=0; DIFF=0; BUILDINFO=0;;
esac;;
*) break;;
esac
case "$TYPE" in
"") ;;
dsc) [ "$FILTERED" = "1" ] && DSC=1 || DSC=0;;
buildinfo) [ "$FILTERED" = "1" ] && BUILDINFO=1 || BUILDINFO=0;;
changes) [ "$FILTERED" = "1" ] &&
{ BCHANGES=1; SCHANGES=1; CHANGES=1; } ||
{ BCHANGES=0; SCHANGES=0; CHANGES=0; } ;;
bchanges) [ "$FILTERED" = "1" ] && BCHANGES=1 || BCHANGES=0;;
schanges) [ "$FILTERED" = "1" ] && SCHANGES=1 || SCHANGES=1;;
deb) [ "$FILTERED" = "1" ] &&
{ ARCHDEB=1; INDEPDEB=1; DEB=1; } ||
{ ARCHDEB=0; INDEPDEB=0; DEB=0; };;
archdeb) [ "$FILTERED" = "1" ] && ARCHDEB=1 || ARCHDEB=0;;
indepdeb) [ "$FILTERED" = "1" ] && INDEPDEB=1 || INDEPDEB=0;;
udeb) [ "$FILTERED" = "1" ] &&
{ ARCHUDEB=1; INDEPUDEB=1; UDEB=1; } ||
{ ARCHUDEB=0; INDEPUDEB=0; UDEB=0; };;
archudeb) [ "$FILTERED" = "1" ] && ARCHUDEB=1 || ARCHUDEB=0;;
indepudeb) [ "$FILTERED" = "1" ] && INDEPUDEB=1 || INDEPUDEB=0;;
tar|orig) [ "$FILTERED" = "1" ] && TARBALL=1 || TARBALL=0;;
diff|debtar) [ "$FILTERED" = "1" ] && DIFF=1 || DIFF=0;;
*) echo "$PROGNAME: Unknown option '$1'" >&2; exit 1;;
esac
shift
done
cmd=
args=""
while [ $# -gt 0 ]; do
arg="$1"
shift
temparg="$(maybe_expand "$arg")"
if [ -z "$temparg" ]; then
if [ -z "$cmd" ]; then
cmd="$arg"
continue
fi
# Not expanded, so simply add to argument list
args="$args
$arg"
else
SEEN_INDEPDEB=0; SEEN_ARCHDEB=0; SEEN_SCHANGES=0; SEEN_BCHANGES=0
SEEN_INDEPUDEB=0; SEEN_ARCHUDEB=0; SEEN_UDEB=0;
SEEN_TARBALL=0; SEEN_DIFF=0; SEEN_DSC=0; SEEN_BUILDINFO=0;
MISSING=0
newarg=""
# Output those items from the expanded list which were
# requested, and record which files are contained in the list
eval "$(echo "$temparg" | while read THISARG; do
if [ -z "$THISARG" ]; then
# Skip
:
elif endswith "$THISARG" _all.deb; then
[ "$INDEPDEB" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_INDEPDEB=1;"
elif endswith "$THISARG" .deb; then
[ "$ARCHDEB" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_ARCHDEB=1;"
elif endswith "$THISARG" _all.udeb; then
[ "$INDEPUDEB" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_INDEPUDEB=1;"
elif endswith "$THISARG" .udeb; then
[ "$ARCHUDEB" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_ARCHUDEB=1;"
elif endswith "$THISARG" .debian.tar.gz || \
endswith "$THISARG" .debian.tar.xz || \
endswith "$THISARG" .debian.tar.bz2; then
[ "$DIFF" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_DIFF=1;"
elif endswith "$THISARG" .tar.bz2 || \
endswith "$THISARG" .tar.gz || \
endswith "$THISARG" .tar.lzma || \
endswith "$THISARG" .tar.xz || \
endswith "$THISARG" .tar.zst || \
endswith "$THISARG" .tar.*.asc; then
[ "$TARBALL" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_TARBALL=1;"
elif endswith "$THISARG" _source.changes; then
[ "$SCHANGES" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_SCHANGES=1;"
elif endswith "$THISARG" .changes; then
[ "$BCHANGES" = "0" ] || echo "newarg\"\$newarg
$THISARG\";"
echo "SEEN_BCHANGES=1;"
elif endswith "$THISARG" .dsc; then
[ "$DSC" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_DSC=1;"
elif endswith "$THISARG" .buildinfo; then
[ "$BUILDINFO" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_BUILDINFO=1;"
elif endswith "$THISARG" .diff.gz; then
[ "$DIFF" = "0" ] || echo "newarg=\"\$newarg
$THISARG\";"
echo "SEEN_DIFF=1;"
elif [ "$FILTERED" != "1" ]; then
# What is it? Output anyway
echo "newarg=\"\$newarg
$THISARG\";"
fi
done)"
INCLUDEARG=1
if endswith "$arg" _source.changes; then
[ "$SCHANGES" = "1" ] || INCLUDEARG=0
SEEN_SCHANGES=1
elif endswith "$arg" .changes; then
[ "$BCHANGES" = "1" ] || INCLUDEARG=0
SEEN_BCHANGES=1
elif endswith "$arg" .dsc; then
[ "$DSC" = "1" ] || INCLUDEARG=0
SEEN_DSC=1
elif endswith "$arg" .buildinfo; then
[ "$BUILDINFO" = "1" ] || INCLUDEARG=0
SEEN_BUILDINFO=1
fi
if [ "$FAIL_MISSING" = "1" ] && [ "$FILTERED" = "1" ]; then
if [ "$CHANGES" = "1" ]; then
if [ "$SEEN_SCHANGES" = "0" ] && [ "$SEEN_BCHANGES" = "0" ]; then
MISSING=1; echo "$arg: .changes fiie not found" >&2
fi
else
if [ "$SCHANGES" = "1" ] && [ "$SEEN_SCHANGES" = "0" ]; then
MISSING=1; echo "$arg: source .changes file not found" >&2
fi
if [ "$BCHANGES" = "1" ] && [ "$SEEN_BCHANGES" = "0" ]; then
MISSING=1; echo "$arg: binary .changes file not found" >&2
fi
fi
if [ "$DEB" = "1" ]; then
if [ "$SEEN_INDEPDEB" = "0" ] && [ "$SEEN_ARCHDEB" = "0" ]; then
MISSING=1; echo "$arg: binary packages not found" >&2
fi
else
if [ "$INDEPDEB" = "1" ] && [ "$SEEN_INDEPDEB" = "0" ]; then
MISSING=1; echo "$arg: arch-indep packages not found" >&2
fi
if [ "$ARCHDEB" = "1" ] && [ "$SEEN_ARCHDEB" = "0" ]; then
MISSING=1; echo "$arg: arch-dep packages not found" >&2
fi
fi
if [ "$UDEB" = "1" ]; then
if [ "$SEEN_INDEPUDEB" = "0" ] && [ "$SEEN_ARCHUDEB" = "0" ]; then
MISSING=1; echo "$arg: udeb packages not found" >&2
fi
else
if [ "$INDEPUDEB" = "1" ] && [ "$SEEN_INDEPUDEB" = "0" ]; then
MISSING=1; echo "$arg: arch-indep udeb packages not found" >&2
fi
if [ "$ARCHUDEB" = "1" ] && [ "$SEEN_ARCHUDEB" = "0" ]; then
MISSING=1; echo "$arg: arch-dep udeb packages not found" >&2
fi
fi
if [ "$BUILDINFO" = "1" ] && [ "$SEEN_BUILDINFO" = "0" ]; then
MISSING=1; echo "$arg: .buildinfo file not found" >&2
fi
if [ "$DSC" = "1" ] && [ "$SEEN_DSC" = "0" ]; then
MISSING=1; echo "$arg: .dsc file not found" >&2
fi
if [ "$TARBALL" = "1" ] && [ "$SEEN_TARBALL" = "0" ]; then
MISSING=1; echo "$arg: upstream tar not found" >&2
fi
if [ "$DIFF" = "1" ] && [ "$SEEN_DIFF" = "0" ]; then
MISSING=1; echo "$arg: Debian debian.tar/diff not found" >&2
fi
[ "$MISSING" = "0" ] || exit 1
fi
args="$args
$newarg"
[ "$INCLUDEARG" = "0" ] || args="$args
$arg"
fi
done
IFS='
'
if [ "$EXTRACT_PACKAGE_NAME" = "1" ]; then
packages=""
for arg in $args; do
packages="$packages
$(echo "$arg" |sed s/_.*//)"
done
args="$packages"
fi
if [ "$SORT" = "1" ]; then
args="$(echo "$args"| sort -)"
fi
if [ "$TAC" = "1" ]; then
args="$(echo "$args"| tac -)"
fi
if [ -z "$cmd" ]; then
for arg in $args; do
echo $arg
done
exit 0
fi
exec $cmd $args

110
scripts/dd-list.1 Normal file
View file

@ -0,0 +1,110 @@
.\" Copyright 2005 Lars Wirzenius
.\"
.\" This program is free software; you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.\" the Free Software Foundation; either version 2 of the License, or
.\" (at your option) any later version.
.\"
.\" This program is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with this program. If not, see <https://www.gnu.org/licenses/>.
.\"
.TH DD\-LIST 1 2011-10-27 "Debian"
.\" --------------------------------------------------------------------
.SH NAME
dd\-list \- nicely list .deb packages and their maintainers
.\" --------------------------------------------------------------------
.SH SYNOPSIS
.BR dd\-list " [" \-hiusV "] [" \-\-help "] [" \-\-stdin "]"
.BR "" "[" "\-\-sources \fISources_file" "]
.BR "" "[" \-\-dctrl "] [" \-\-version "] [" \-\-uploaders "] [" \fIpackage " ...]"
.\" --------------------------------------------------------------------
.SH DESCRIPTION
.B dd\-list
produces nicely formatted lists of Debian (.deb) packages and their
maintainers.
.PP
Input is a list of source or binary package names on the command line
(or the standard input if
.B \-\-stdin
is given).
Output is a list of the following format, where package names are source
packages by default:
.PP
.nf
.RS
J. Random Developer <jrandom@debian.org>
.RS
j-random-package
j-random-other
.RE
.PP
Diana Hacker <diana@example.org>
.RS
fun-package
more-fun-package
.RE
.RE
.fi
.PP
This is useful when you want, for example, to produce a list of packages
that need to attention from their maintainers, e.g., to be rebuilt when
a library version transition happens.
.\" --------------------------------------------------------------------
.SH OPTIONS
.TP
.BR \-h ", " \-\-help
Print brief help message.
.TP
.BR \-i ", " \-\-stdin
Read package names from the standard input, instead of taking them
from the command line. Package names are whitespace delimited.
.TP
.BR \-d ", " \-\-dctrl
Read package list from standard input in the format of a Debian
package control file. This includes the status file, or output of
apt-cache. This is the fastest way to use dd-list, as it uses the
maintainer information from the input instead of looking up the maintainer
of each listed package.
.IP
If no \fISource:\fP line is given, the \fIPackage:\fP name is used for
output, which might be a binary package name.
.TP
.BR \-z ", " \-\-uncompress
Try to uncompress the \-\-dctrl input before parsing. Supported compression
formats are gz, bzip2 or xz.
.TP
\fB\-s\fR, \fB\-\-sources\fR \fISources_file\fR
Read package information from the specified \fISources_file\fRs. This can be
given multiple times. The files can be gz, bzip2 or xz compressed. If the
filename does not end in \fI.gz\fR, \fI.bz2\fR or \fI.xz\fR, then the \fB-z\fR
option must be used.
.IP
If no \fISources_file\fRs are specified, dd\-list will ask apt\-get for
an appropriate set of sources (if \fIapt\fR is at version greater than 1.1.8),
else any files matching \fI/var/lib/apt/lists/*_source_Sources\fR will be used.
.TP
.BR \-u ", " \-\-uploaders
Also list developers who are named as uploaders of packages, not only
the maintainers; this is the default behaviour, use \-\-nouploaders to
prevent it. Uploaders are indicated with "(U)" appended to the package name.
.TP
.BR \-nou ", " \-\-nouploaders
Only list package Maintainers, do not list Uploaders.
.TP
.BR \-b ", " \-\-print\-binary
Use binary package names in the output instead of source package names
(has no effect with \fB--dctrl\fP if the \fIPackage:\fP line contains
source package names).
.TP
.BR \-V ", " \-\-version
Print the version.
.\" --------------------------------------------------------------------
.SH AUTHOR
Lars Wirzenius <liw@iki.fi>
.P
Joey Hess <joeyh@debian.org>

322
scripts/dd-list.pl Executable file
View file

@ -0,0 +1,322 @@
#!/usr/bin/perl
#
# dd-list: Generate a list of maintainers of packages.
#
# Written by Joey Hess <joeyh@debian.org>
# Modifications by James McCoy <jamessan@debian.org>
# Based on a python implementation by Lars Wirzenius.
# Copyright 2005 Lars Wirzenius, Joey Hess
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use strict;
use warnings;
use FileHandle;
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use Dpkg::Version;
use Dpkg::IPC;
my $uncompress;
BEGIN {
$uncompress = eval {
require IO::Uncompress::AnyUncompress;
IO::Uncompress::AnyUncompress->import('$AnyUncompressError');
1;
};
}
my $version = '###VERSION###';
sub normalize_package {
my $name = shift;
# Remove any arch-qualifier
$name =~ s/:.*//;
return lc($name);
}
sub sort_developers {
return map { $_->[0] }
sort { $a->[1] cmp $b->[1] }
map { [$_, uc] } @_;
}
sub help {
print <<"EOF";
Usage: dd-list [options] [package ...]
-h, --help
Print this help text.
-i, --stdin
Read package names from the standard input.
-d, --dctrl
Read package list in Debian control data from standard input.
-z, --uncompress
Try to uncompress the --dctrl input before parsing. Supported
compression formats are gz, bzip2 and xz.
-s, --sources SOURCES_FILE
Read package information from given SOURCES_FILE instead of all files
matching /var/lib/apt/lists/*_source_Sources. Can be specified
multiple times. The files can be gz, bzip2 or xz compressed.
-u, --uploaders
Also list Uploaders of packages, not only the listed Maintainers
(this is the default behaviour, use --nouploaders to prevent this).
-nou, --nouploaders
Only list package Maintainers, do not list Uploaders.
-b, --print-binary
If binary package names are given as input, print these names
in the output instead of corresponding source packages.
-V, --version
Print version (it\'s $version by the way).
EOF
}
my $use_stdin = 0;
my $use_dctrl = 0;
my $source_files = [];
my $show_uploaders = 1;
my $opt_uncompress = 0;
my $print_binary = 0;
GetOptions(
"help|h" => sub { help(); exit },
"stdin|i" => \$use_stdin,
"dctrl|d" => \$use_dctrl,
"sources|s=s@" => \$source_files,
"uploaders|u!" => \$show_uploaders,
'z|uncompress' => \$opt_uncompress,
"print-binary|b" => \$print_binary,
"version" => sub { print "dd-list version $version\n" })
or do {
help();
exit(1);
};
if ($opt_uncompress && !$uncompress) {
warn
"You must have the libio-compress-perl package installed to use the -z option.\n";
exit 1;
}
my %dict;
my $errors = 0;
my %package_name;
sub parsefh {
my ($fh, $fname, $check_package) = @_;
local $/ = "\n\n";
my $package_names;
if ($check_package) {
$package_names = sprintf '(?:^| )(%s)(?:,|$)',
join '|', map { "\Q$_\E" }
keys %package_name;
}
my %sources;
while (<$fh>) {
my ($package, $source, $binaries, $maintainer, @uploaders);
# These source packages are only kept around because of stale binaries
# on old archs or due to Built-Using relationships.
if (/^Extra-Source-Only:\s+yes/m) {
next;
}
# Binary is shown in _source_Sources and contains all binaries produced by
# that source package
if (/^Binary:\s+(.*(?:\n .*)*)$/m) {
$binaries = $1;
$binaries =~ s/\n//;
}
# Package is shown both in _source_Sources and _binary-*. It is the
# name of the package, source or binary respectively, being described
# in that control stanza
if (/^Package:\s+(.*)$/m) {
$package = $1;
}
# Source is shown in _binary-* and specifies the source package which
# produced the binary being described
if (/^Source:\s+(.*)$/m) {
$source = $1;
}
if (/^Maintainer:\s+(.*)$/m) {
$maintainer = $1;
}
if (/^Uploaders:\s+(.*(?:\n .*)*)$/m) {
my $matches = $1;
$matches =~ s/\n//g;
@uploaders = split /(?<=>)\s*,\s*/, $matches;
}
my $version = '0~0~0';
if (/^Version:\s+(.*)$/m) {
$version = $1;
}
if (defined $maintainer
&& (defined $package || defined $source || defined $binaries)) {
$source ||= $package;
$binaries ||= $package;
my @names;
if ($check_package) {
my @pkgs;
if (@pkgs = ($binaries =~ m/$package_names/g)) {
$sources{$source}{$version}{binaries} = [@pkgs];
} elsif ($source !~ m/$package_names/) {
next;
}
} else {
$sources{$source}{$version}{binaries} = [$binaries];
}
$sources{$source}{$version}{maintainer} = $maintainer;
$sources{$source}{$version}{uploaders} = [@uploaders];
} else {
warn "E: parse error in stanza $. of $fname\n";
$errors = 1;
}
}
for my $source (keys %sources) {
my @versions
= sort map { Dpkg::Version->new($_) } keys %{ $sources{$source} };
my $version = $versions[-1];
my $srcinfo = $sources{$source}{$version};
my @names;
if ($check_package) {
$package_name{$source}--;
$package_name{$_}-- for @{ $srcinfo->{binaries} };
}
@names = $print_binary ? @{ $srcinfo->{binaries} } : $source;
push @{ $dict{ $srcinfo->{maintainer} } }, @names;
if ($show_uploaders && @{ $srcinfo->{uploaders} }) {
foreach my $uploader (@{ $srcinfo->{uploaders} }) {
push @{ $dict{$uploader} }, map "$_ (U)", @names;
}
}
}
}
if ($use_dctrl) {
my $fh;
if ($uncompress) {
$fh = IO::Uncompress::AnyUncompress->new('-')
or die "E: Unable to decompress STDIN: $AnyUncompressError\n";
} else {
$fh = \*STDIN;
}
parsefh($fh, 'STDIN');
} else {
my @packages;
if ($use_stdin) {
while (my $line = <STDIN>) {
chomp $line;
$line =~ s/^\s+|\s+$//g;
push @packages, split(' ', $line);
}
} else {
@packages = @ARGV;
}
for my $name (@packages) {
$package_name{ normalize_package($name) } = 1;
}
my $apt_version;
spawn(
exec => ['dpkg-query', '-W', '-f', '${source:Version}', 'apt'],
to_string => \$apt_version,
wait_child => 1,
nocheck => 1
);
my $useAptHelper = 0;
if (defined $apt_version) {
$useAptHelper
= version_compare_relation($apt_version, REL_GE, '1.1.8');
}
unless (@{$source_files}) {
if ($useAptHelper) {
my ($sources, $err);
spawn(
exec => [
'apt-get', 'indextargets',
'--format', '$(FILENAME)',
'Created-By: Sources'
],
to_string => \$sources,
error_to_string => \$err,
wait_child => 1,
nocheck => 1
);
if ($? >> 8) {
die "Unable to get list of Sources files from apt: $err\n";
}
$source_files = [split(/\n/, $sources)];
} else {
$source_files = [glob('/var/lib/apt/lists/*_source_Sources')];
}
}
foreach my $source (@{$source_files}) {
my $fh;
if ($useAptHelper) {
my $good = open($fh, '-|', '/usr/lib/apt/apt-helper', 'cat-file',
$source);
if (!$good) {
warn
"E: Couldn't run apt-helper to get contents of '$source': $!\n";
$errors = 1;
next;
}
} else {
if ($opt_uncompress
|| ($uncompress && $source =~ m/\.(?:gz|bz2|xz)$/)) {
$fh = IO::Uncompress::AnyUncompress->new($source);
} else {
$fh = FileHandle->new("<$source");
}
unless (defined $fh) {
warn "E: Couldn't open $source\n";
$errors = 1;
next;
}
}
parsefh($fh, $source, 1);
close $fh;
}
}
foreach my $developer (sort_developers(keys %dict)) {
print "$developer\n";
my %seen;
foreach my $package (sort @{ $dict{$developer} }) {
next if $seen{$package};
$seen{$package} = 1;
print " $package\n";
}
print "\n";
}
foreach my $package (grep { $package_name{$_} > 0 } keys %package_name) {
warn "E: Unknown package: $package\n";
$errors = 1;
}
exit($errors);

178
scripts/deb-check-file-conflicts Executable file
View file

@ -0,0 +1,178 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# pylint: disable=invalid-name
# Copyright © 2016 Maximiliano Curia <maxy@gnuservers.com.ar>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
"""
Check the produced binary packages and checks if there are conflicting files
against packages that are not declared with breaks and replaces.
Note that the results depend on what apt repositories are enabled. If you want
prevent upgrade issues across Debian releases, you need to have both unstable
and the previous release repositories enabled in the apt sources configuration.
"""
import argparse
import collections
import logging
import os
import subprocess
import sys
from debian import deb822, debfile, debian_support
from junit_xml import TestCase, TestSuite
def get_pkg_file(lines):
found = collections.defaultdict(set)
for line in lines.split("\n"):
if not line:
continue
package, filename = line.split(": ", 1)
found[package].add(filename)
return found
def get_relations(field_value):
relations = collections.defaultdict(lambda: collections.defaultdict(str))
if not field_value:
return relations
parsed = deb822.PkgRelation.parse_relations(field_value)
for or_part in parsed:
for part in or_part:
rel_name = part["name"]
if "version" in part:
if "version" in relations[rel_name]:
if (
debian_support.version_compare(
part["version"][1], relations[rel_name]["version"]
)
> 0
):
relations[rel_name]["version"] = part["version"][1]
else:
relations[rel_name] = collections.defaultdict(str)
return relations
def process_options():
kw = {"format": "[%(levelname)s] %(message)s"}
arg_parser = argparse.ArgumentParser(description=__doc__)
arg_parser.add_argument("--debug", action="store_true")
arg_parser.add_argument(
"--changes-file", default=os.environ.get("CHANGES_FILE", "")
)
arg_parser.add_argument(
"-o",
"--output",
help="Output file",
default=f"{os.environ.get('EXPORT_DIR', '.')}/missing_breaks_replaces.xml",
)
args = arg_parser.parse_args()
if args.debug:
kw["level"] = logging.DEBUG
logging.basicConfig(**kw)
return args
def get_package_relations(deb_control):
"""Extract and parse package relation fields"""
deb_replaces = deb_control.get("Replaces", "")
deb_breaks = deb_control.get("Breaks", "")
deb_conflicts = deb_control.get("Conflicts", "")
return {
"breaks": get_relations(deb_breaks),
"conflicts": get_relations(deb_conflicts),
"replaces": get_relations(deb_replaces),
}
def process_entry(dirname, entry):
"""Process a single changes files entry"""
logging.debug(entry["name"])
deb_filename = os.path.join(dirname, entry["name"])
deb_control = debfile.DebFile(deb_filename).debcontrol()
name = deb_control["Package"]
logging.info("Processing: %s %s", name, deb_control["Version"])
relations = get_package_relations(deb_control)
# apt-file now returns 1 if the files are not found, specially bothering
# with the dbgsym packages (and the packages not yet uploaded)
proc = subprocess.run(
["apt-file", "-D", "search", deb_filename],
universal_newlines=True,
stdout=subprocess.PIPE,
check=False,
)
if proc.returncode and proc.stdout:
proc.check_returncode()
interesting = get_pkg_file(proc.stdout)
result = []
for pkg_name in interesting:
if pkg_name == name or pkg_name in relations["conflicts"]:
# TODO check versions
continue
if pkg_name in relations["breaks"] and pkg_name in relations["replaces"]:
# TODO check versions
continue
msg = f"{name} conflicts with {pkg_name} files: {interesting[pkg_name]}"
logging.error("Missing Breaks/Replaces found")
logging.error(msg)
result.append(msg)
return name, result, proc.stdout
def generate_test_cases(results):
test_cases = []
for name, (result, output) in results.items():
test_case = TestCase(name, stdout=output)
if result:
test_case.add_error_info("\n".join(result))
test_cases.append(test_case)
return test_cases
def main():
"""Check changes files for missing Breaks/Replaces using apt-file"""
args = process_options()
dirname = os.path.dirname(args.changes_file)
results = {}
with open(args.changes_file, encoding="utf-8") as changes_file:
changes = deb822.Changes(changes_file)
for entry in changes["Files"]:
if not entry["name"].endswith(".deb"):
continue
name, result, output = process_entry(dirname, entry)
results[name] = (result, output)
test_cases = generate_test_cases(results)
test_suite = TestSuite("check_for_missing_breaks_replaces", test_cases)
with open(args.output, "w", encoding="utf-8") as output_file:
output_file.write(TestSuite.to_xml_string([test_suite]))
return 1 if any(test_case.is_error() for test_case in test_cases) else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,72 @@
.\" Copyright (c) 2016 Maximiliano Curia <maxy@gnuservers.com.ar>
.\"
.\" This program is free software; you can redistribute it and/or
.\" modify it under the terms of the GNU General Public License
.\" as published by the Free Software Foundation; either version 2
.\" of the License, or (at your option) any later version.
.\"
.\" This program is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" See file /usr/share/common-licenses/GPL-2 for more details.
.\"
.TH "DEB\-CHECK\-MISSING\-BREAKS\-REPLACES\-CONFLICTS" 1 "Debian Utilities" "DEBIAN"
.SH NAME
deb-check-file-conflicts \- check for missing Breaks/Replaces in Debian packages
.SH SYNOPSIS
.B deb-check-file-conflicts
[\fB\-\-debug\fR]
[\fB\-\-changes\-file\fR=\fIFILE\fR]
[\fB\-o\fR \fIOUTPUT\fR]
[\fB\-\-output\fR=\fIOUTPUT\fR]
.SH DESCRIPTION
.B deb-check-file-conflicts
checks produced binary packages for conflicting files against packages that are not
declared with Breaks and Replaces relationships.
.PP
The tool helps identify potential upgrade issues where files from one package
might conflict with files from other packages during installation.
.PP
Note that the results depend on what apt repositories are enabled. If you want
to prevent upgrade issues across Debian releases, you need to have both unstable
and the previous release repositories enabled in the apt sources configuration.
.SH OPTIONS
.TP
.B \-\-debug
Enable debug output
.TP
.B \-\-changes\-file=\fIFILE\fR
Specify the changes file to check (defaults to CHANGES_FILE environment variable)
.TP
.BR \-o ", " \-\-output =\fIFILE\fR
Write output to FILE (defaults to $EXPORT_DIR/missing_breaks_replaces.xml or ./missing_breaks_replaces.xml)
.SH OUTPUT
The tool generates a JUnit XML format report containing test results for each
binary package checked. Each test case indicates whether missing Breaks/Replaces
relationships were found.
.SH EXAMPLES
.EX
# Check a changes file for missing relationships:
$ deb-check-file-conflicts --changes-file foo_1.0-1_amd64.changes
# Write results to a specific output file:
$ deb-check-file-conflicts -o results.xml --changes-file foo_1.0-1_amd64.changes
.EE
.SH AUTHOR
\fBdeb-check-file-conflicts\fR and this manual page were written by
Maximiliano Curia <maxy@gnuservers.com.ar>.
.PP
Both are released under the GNU General Public License, version 2 or later.
.SH SEE ALSO
.BR dpkg (1),
.BR apt\-file (1)

322
scripts/deb-janitor Executable file
View file

@ -0,0 +1,322 @@
#!/usr/bin/python3
# Copyright (c) 2020 Jelmer Vernooij <jelmer@debian.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See file /usr/share/common-licenses/GPL-3 for more details.
#
# pylint: disable=invalid-name
# pylint: enable=invalid-name
"""
Command-line interface for the Debian Janitor.
See https://janitor.debian.net/
"""
import argparse
import json
import logging
import subprocess
import sys
from typing import Any, Optional
from urllib.error import HTTPError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
from debian.changelog import Changelog
import devscripts
DEFAULT_API_URL = "https://janitor.debian.net/api/"
USER_AGENT = f"devscripts janitor cli ({devscripts.__version__})"
DEFAULT_URLLIB_TIMEOUT = 30
def _get_json_url(http_url: str, timeout: int = DEFAULT_URLLIB_TIMEOUT) -> Any:
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
logging.debug("Retrieving %s", http_url)
with urlopen(Request(http_url, headers=headers), timeout=timeout) as resp:
http_contents = resp.read()
return json.loads(http_contents)
def schedule(source, campaign, api_url=DEFAULT_API_URL):
"""Schedule a new run for a package.
Args:
source: the source package name
campaign: the campaign to schedule for
"""
url = f"{api_url}{quote(campaign)}/pkg/{quote(source)}/schedule"
headers = {"User-Agent": USER_AGENT}
req = Request(url, headers=headers, method="POST")
try:
with urlopen(req) as resp:
resp = json.load(resp)
except HTTPError as err:
if err.code == 404:
raise NoSuchSource(json.loads(err.read())["reason"]) from err
raise
estimated_duration = resp["estimated_duration_seconds"]
queue_position = resp["queue_position"]
queue_wait_time = resp["queue_wait_time"]
return (estimated_duration, queue_position, queue_wait_time)
class MissingDiffError(Exception):
"""There is no diff for the specified package/campaign combination."""
class NoSuchSource(Exception):
"""There is no source package known with the specified name."""
def diff(source, campaign, api_url=DEFAULT_API_URL):
"""Retrieve the source diff for a package/campaign.
Args:
source: the source package name
campaign: the campaign to retrieve
Returns:
the diff as a bytestring
Raises:
MissingDiffError: If the diff was missing
(source not valid, campaign not valid, no runs yet, etc)
"""
url = f"{api_url}{quote(campaign)}/pkg/{quote(source)}/diff"
headers = {"User-Agent": USER_AGENT, "Accept": "text/plain"}
req = Request(url, headers=headers)
try:
with urlopen(req) as resp:
data = resp.read()
except HTTPError as err:
if err.code == 404:
raise MissingDiffError(err.read().decode()) from err
raise err
return data
def merge(
source: str, campaign: str, api_url: str = DEFAULT_API_URL, force: bool = False
): # pylint: disable=R0915
"""Merge changes from a campaign.
Args:
source: the source package name
campaign: applicable campaign
api_url: API URL
"""
url = f"{api_url}{quote(campaign)}/pkg/{quote(source)}"
try:
result = _get_json_url(url)
except HTTPError as err:
if err.code == 404:
logging.warning("No runs for %s/%s", source, campaign)
return 1
raise
if result["result_code"] != "success":
if force:
logging.fatal(
"Last run was not successful: %s; run with --force to merge anyway.",
result["result_code"],
)
return 1
logging.warning("Last run was not success: %s, merging anyway.")
remotes = subprocess.check_output(["git", "remote"], text=True).splitlines(False)
if "debian-janitor" not in remotes:
logging.info("Adding debian-janitor remote")
subprocess.check_call(
[
"git",
"remote",
"add",
"debian-janitor",
f"https://janitor.debian.net/git/{source}",
]
)
else:
logging.debug("debian-janitor already remote exists")
if len(result["branches"]) > 1:
logging.fatal(
"Merging changes with multiple branches is currently not supported"
)
return 1
if len(result["branches"]) < 1:
logging.fatal("No branches to merge")
return 1
# TODO(jelmer): Fetch tags
ret = 0
for role, _details in result["branches"].items():
try:
subprocess.check_call(
["git", "pull", "debian-janitor", f"{campaign}/{role or 'main'}"]
)
except subprocess.CalledProcessError:
# Git would have already printed an error to stderr
ret = 1
return ret
def review(
source: str,
campaign: str,
verdict: str,
comment: Optional[str] = None,
api_url=DEFAULT_API_URL,
) -> int:
"""Submit a review of a package.
Args:
source: the source package name
campaign: applicable campaign
verdict: a verdict ("approved", "abstained", "rejected", "reschedule")
comment: optional comment explaining the verdict
"""
url = f"{api_url}{quote(campaign)}/pkg/{quote(source)}"
headers = {"User-Agent": USER_AGENT, "Accept": "text/plain"}
data = {"review-status": verdict}
if comment:
data["review-comment"] = comment
req = Request(url, headers=headers, method="POST", data=urlencode(data).encode())
with urlopen(req) as resp:
resp.read()
return 0
def status(source: str, campaign: str, api_url: str = DEFAULT_API_URL) -> int:
"""Print the status for a package.
Args:
source: the source package name
campaign: applicable campaign
"""
url = f"{api_url}{quote(campaign)}/pkg/{quote(source)}"
try:
data = _get_json_url(url)
except HTTPError as err:
if err.code == 404:
logging.info("No relevant runs.")
# TODO(jelmer): print info about next scheduled run and command?
return 2
raise
if data.get("failure"):
if data["failure"]["transient"]:
transient = " (transient)"
else:
transient = ""
logging.warning("Failure stage: %s%s", data["failure"]["stage"], transient)
return 1
return 0
def main(argv): # pylint: disable=R0911,R0912,R0915
"""Handle command-line arguments."""
parser = argparse.ArgumentParser("janitor")
parser.add_argument("--debug", action="store_true")
parser.add_argument(
"--api-url", type=str, help="API endpoint to talk to", default=DEFAULT_API_URL
)
subparsers = parser.add_subparsers(help="sub-command help", dest="subcommand")
schedule_parser = subparsers.add_parser("schedule")
schedule_parser.add_argument("campaign")
schedule_parser.add_argument("source", help="Source package name", nargs="?")
diff_parser = subparsers.add_parser("diff")
diff_parser.add_argument("campaign")
diff_parser.add_argument("source", help="Source package name", nargs="?")
merge_parser = subparsers.add_parser("merge")
merge_parser.add_argument("campaign")
review_parser = subparsers.add_parser("review")
review_parser.add_argument("campaign")
review_parser.add_argument("--source", help="Source package name")
review_parser.add_argument(
"verdict",
help="Verdict",
choices=["approved", "rejected", "abstained", "reschedule"],
type=str,
)
review_parser.add_argument("comment", help="Comment explaining review", nargs="?")
status_parser = subparsers.add_parser("status")
status_parser.add_argument("campaign")
status_parser.add_argument("source", help="Source package name", nargs="?")
args = parser.parse_args(argv)
logging.basicConfig(
format="%(message)s", level=logging.INFO if not args.debug else logging.DEBUG
)
def _get_local_source() -> str:
try:
with open("debian/changelog", "r", encoding="utf-8") as changelog_file:
changelog = Changelog(changelog_file)
except FileNotFoundError:
parser.error("not in Debian package, and no source package name specified")
logging.info("Using source package: %s", changelog.package)
return changelog.package
if args.subcommand == "schedule":
if args.source is None:
args.source = _get_local_source()
try:
(est_duration, pos, wait_time) = schedule(
args.source, args.campaign, api_url=args.api_url
)
except NoSuchSource as err:
logging.fatal("%s", err.args[0])
return 1
if pos is not None:
logging.info(
"Scheduled. Estimated duration: %.2fs,"
" queue position: %d (wait time: %.2f)",
est_duration,
pos,
wait_time,
)
else:
logging.info("Scheduled.")
return 0
if args.subcommand == "diff":
if args.source is None:
args.source = _get_local_source()
try:
sys.stdout.buffer.write(
diff(args.source, args.campaign, api_url=args.api_url)
)
sys.stdout.flush()
except MissingDiffError as err:
logging.fatal("%s", err.args[0])
return 1
return 0
if args.subcommand == "merge":
source = _get_local_source()
return merge(source, args.campaign, api_url=args.api_url)
if args.subcommand == "review":
if args.source is None:
args.source = _get_local_source()
return review(
args.source, args.campaign, args.verdict, args.comment, api_url=args.api_url
)
if args.subcommand == "status":
if args.source is None:
args.source = _get_local_source()
return status(args.source, args.campaign, api_url=args.api_url)
parser.print_usage()
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

94
scripts/deb-janitor.1 Normal file
View file

@ -0,0 +1,94 @@
.\" Copyright (c) 2020 Jelmer Vernooij <jelmer@debian.org>
.\"
.\" This program is free software; you can redistribute it and/or
.\" modify it under the terms of the GNU General Public License
.\" as published by the Free Software Foundation; either version 3
.\" of the License, or (at your option) any later version.
.\"
.\" This program is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" See file /usr/share/common-licenses/GPL-3 for more details.
.\"
.TH "DEB\-JANITOR" 1 "Debian Utilities" "DEBIAN"
.SH NAME
deb-janitor \- interact with the Debian Janitor service
.SH SYNOPSIS
.TP
.B deb-janitor status CAMPAIGN SOURCE?
.TP
.B deb-janitor diff CAMPAIGN SOURCE?
.TP
.B deb-janitor schedule CAMPAIGN SOURCE?
.TP
.B deb-janitor merge [--force] CAMPAIGN
.TP
.B deb-janitor review CAMPAIGN [--source SOURCE] rejected|approved|reschedule COMMENT?
.SH DESCRIPTION
.B deb-janitor
is a command-line client for the Debian Janitor service, interacting
with the API. It currently allows retrieving the diff for
specific packages or scheduling new runs.
.PP
\fBCAMPAIGN\fR is the name of one of the campaigns supported by the janitor. Common values
include \fIlintian-fixes\fR and \fImultiarch-fixes\fR. See the homepage for a
full list.
.PP
\fBSOURCE\fR is the name of a source package. If no source package name is specified,
the source name is retrieved from debian/changelog in the current directory.
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
show this help message and exit
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Output more information
.TP
\fB\-\-api-url\fR
Override the API endpoint to communicate with, rather than using the
main Debian Janitor instance. E.g. --api-url=https://janitor.kali.org/api/.
.SH EXAMPLES
.EX
# Schedule a new run fixing lintian issues in the "dulwich" package:
$ deb-janitor schedule dulwich lintian-fixes
Scheduled. Estimated duration: 236.32s, queue position: 1 (wait time: 0.00)
# Retrieve the diff for fontmake
$ deb-janitor diff fontmake lintian-fixes
=== added file 'debian/upstream/metadata'
--- a/debian/upstream/metadata 1970-01-01 00:00:00 +0000
+++ b/debian/upstream/metadata 2020-11-28 11:58:34 +0000
@@ -0,0 +1,5 @@
+---
+Bug-Database: https://github.com/googlei18n/fontmake/issues
+Bug-Submit: https://github.com/googlei18n/fontmake/issues/new
+Repository: https://github.com/googlei18n/fontmake.git
+Repository-Browse: https://github.com/googlei18n/fontmake
# Leave a review for a package
$ deb-janitor review fontmake lintian-fixes rejected "Some fonts are no longer installed"
# Merge lintian-fixes for a package
$ debcheckout a56
$ cd a56
$ deb-janitor merge lintian-fixes
Adding debian-janitor remote
.EE
.SH AUTHORS
\fBdeb-janitor\fR and this manual page were written by Jelmer Vernooij
<jelmer@debian.org>
.PP
Both are released under the GNU General Public License, version 3 or later.
.SH SEE ALSO
.BR lintian-brush (1)

320
scripts/deb-reversion.dbk Normal file
View file

@ -0,0 +1,320 @@
<?xml version='1.0' encoding='ISO-8859-1'?>
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" [
<!--
Process this file with an XSLT processor: `xsltproc \
-''-nonet /usr/share/sgml/docbook/stylesheet/xsl/nwalsh/\
manpages/docbook.xsl manpage.dbk'. A manual page
<package>.<section> will be generated. You may view the
manual page with: nroff -man <package>.<section> | less'. A
typical entry in a Makefile or Makefile.am is:
DB2MAN=/usr/share/sgml/docbook/stylesheet/xsl/nwalsh/\
manpages/docbook.xsl
XP=xsltproc -''-nonet
manpage.1: manpage.dbk
$(XP) $(DB2MAN) $<
The xsltproc binary is found in the xsltproc package. The
XSL files are in docbook-xsl. Please remember that if you
create the nroff version in one of the debian/rules file
targets (such as build), you will need to include xsltproc
and docbook-xsl in your Build-Depends control field.
-->
<!-- Fill in your name for FIRSTNAME and SURNAME. -->
<!ENTITY dhfirstname "<firstname>martin f.</firstname>">
<!ENTITY dhsurname "<surname>krafft</surname>">
<!ENTITY dhmaintfirstname "<firstname>Julian</firstname>">
<!ENTITY dhmaintsurname "<surname>Gilbey</surname>">
<!-- Please adjust the date whenever revising the manpage. -->
<!ENTITY dhdate "<date>Feb 13, 2006</date>">
<!-- SECTION should be 1-8, maybe w/ subsection other parameters are
allowed: see man(7), man(1). -->
<!ENTITY dhsection "<manvolnum>1</manvolnum>">
<!ENTITY dhemail "<email>madduck@debian.org</email>">
<!ENTITY dhmaintemail "<email>jdg@debian.org</email>">
<!ENTITY dhusername "martin f. krafft">
<!ENTITY dhmaintusername "Julian Gilbey">
<!ENTITY dhucpackage "<refentrytitle>deb-reversion</refentrytitle>">
<!ENTITY dhpackage "deb-reversion">
<!ENTITY dhcommand "deb-reversion">
<!ENTITY debian "<productname>Debian</productname>">
<!ENTITY gnu "<acronym>GNU</acronym>">
<!ENTITY gpl "&gnu; <acronym>GPL</acronym>">
]>
<refentry>
<refentryinfo>
<address>
&dhemail;
</address>
&dhdate;
</refentryinfo>
<refmeta>
&dhucpackage;
&dhsection;
</refmeta>
<refnamediv>
<refname>&dhcommand;</refname>
<refpurpose>simple script to change the version of a .deb file</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>&dhcommand;</command>
<arg choice="opt">
<replaceable>options</replaceable>
</arg>
<replaceable> .deb-file</replaceable>
<arg choice="opt" rep="repeat"><replaceable>log message</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>DESCRIPTION</title>
<para>
<command>&dhcommand;</command> unpacks the specified <filename>.deb</filename> file, changes the version
number in the relevant locations, appends a Debian
<filename>changelog</filename> entry with the specified
contents, and creates a new <filename>.deb</filename> file with the updated version.
</para>
<para>
By default, the tool creates a new version number suitable for
local changes, such that the new package will be greater than
the current one, but lesser than any future, official Debian
packages. With <option>-v <replaceable
class="parameter">version</replaceable></option>, the version
number can be specified directly. On the other hand, the
<option>-c</option> simply calculates the new version number but
does not generate a new package.
</para>
<para>
When building a <filename>.deb</filename> file, root privileges are required in order
to have the correct permissions and ownerships in the resulting
<filename>.deb</filename> file. This can be achieved either by running
<command>&dhcommand;</command> as root or running under
<citerefentry><refentrytitle>fakeroot</refentrytitle>
<manvolnum>1</manvolnum></citerefentry>, as 'fakeroot
&dhcommand; foo.deb'.
</para>
<para>
With <option>-k <replaceable
class="parameter">hook</replaceable></option>, a hook script may
be specified, which is run on the unpacked binary packages just
before it is repacked. If you want to write changelog entries
from within the hook, use '<command>dch -a -- <replaceable
class="parameter">your message</replaceable></command>'.
(Alternatively, do not give a changelog entry on the
<command>&dhcommand;</command> command line and
<command>dch</command> will be called automatically.) The hook
command must be placed in quotes if it has more than one word;
it is called via <command>sh -c</command>.
</para>
</refsect1>
<refsect1>
<title>OPTIONS</title>
<variablelist>
<varlistentry>
<term><option>-v</option> <replaceable class="parameter">new-version</replaceable></term>
<term><option>--new-version</option> <replaceable class="parameter">new-version</replaceable></term>
<listitem>
<para>
Specifies the version number to be used for the new
version. Passed to <citerefentry>
<refentrytitle>dch</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-o</option> <replaceable class="parameter">old-version</replaceable></term>
<term><option>--old-version</option> <replaceable class="parameter">old-version</replaceable></term>
<listitem>
<para>
Specifies the version number to be used as the old
version instead of the version stored in the <filename>.deb</filename>'s
<filename>control</filename> file.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-c</option></term>
<term><option>--calculate-only</option></term>
<listitem>
<para>
Only calculate and display the new version number which
would be used; do not build a new <filename>.deb</filename> file. Cannot be
used in conjunction with <option>-v</option>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-s</option> <replaceable class="parameter">string</replaceable></term>
<term><option>--string</option> <replaceable class="parameter">string</replaceable></term>
<listitem>
<para>
Instead of using 'LOCAL.' as the version string to append
to the old version number, use <replaceable
class="parameter">string</replaceable> instead.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-k</option> <replaceable class="parameter">hook-command</replaceable></term>
<term><option>--hook</option> <replaceable class="parameter">hook-command</replaceable></term>
<listitem>
<para>
A hook command to run after unpacking the old <filename>.deb</filename> file and
modifying the <filename>changelog</filename>, and before packing up the new <filename>.deb</filename>
file. Must be in quotes if it is more than one (shell)
word. Only one hook command may be specified; if you want
to perform more than this, you could specify 'bash' as the
hook command, and you will then be given a shell to work
in.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-D</option></term>
<term><option>--debug</option></term>
<listitem>
<para>
Pass <option>--debug</option> to
<citerefentry>
<refentrytitle>dpkg-deb</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-b</option></term>
<term><option>--force-bad-version</option></term>
<listitem>
<para>
Pass <option>--force-bad-version</option> to
<citerefentry>
<refentrytitle>dch</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-h</option></term>
<term><option>--help</option></term>
<listitem>
<para>
Display usage information.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-V</option></term>
<term><option>--version</option></term>
<listitem>
<para>
Display version information.
</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>SEE ALSO</title>
<para>
<citerefentry>
<refentrytitle>dch</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>,
<citerefentry>
<refentrytitle>dpkg-deb</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>,
<citerefentry>
<refentrytitle>fakeroot</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
</para>
</refsect1>
<refsect1>
<title>DISCLAIMER</title>
<para>
&dhpackage; is a tool intended to help porters with
modifying packages for other architectures, and to augment deb-repack,
which creates modified packages with identical version numbers as the
official packages. Chaos will ensue! With &dhpackage;, a proper version
number can be selected, which does not obstruct the next official
release but can be specifically pinned with APT or held with dpkg.
</para>
<para>
Please take note that &dhpackage; does not come without problems. While
it works fine in most cases, it may just not in yours. Especially,
please consider that it changes binary packages (only!) and hence can
break strict versioned dependencies between binary packages generated
from the same source. </para>
<para>
You are using this tool at your own risk and I shall not shed a tear if
your gerbil goes up in flames, your microwave attacks the stereo, or the
angry slamming of your fist spills your coffee into the keyboard, which
sets off a chain reaction resulting in a vast amount of money transferred
from your account to mine.
</para>
</refsect1>
<refsect1>
<title>AUTHOR</title>
<para>
&dhpackage; is Copyright 2004-5 by &dhusername; &dhemail; and
modifications are Copyright 2006 by &dhmaintusername; &dhmaintemail;.
</para>
<para>
Permission is granted to copy, distribute and/or modify this document
under the terms of the Artistic License:
<ulink>http://www.opensource.org/licenses/artistic-license.php</ulink>.
On Debian systems, the complete text of the Artistic License can be
found
in <filename>/usr/share/common-licenses/Artistic</filename>.
</para>
<para>
This manual page was written by &dhusername; &dhemail; and
modified by &dhmaintusername; &dhmaintemail;.
</para>
</refsect1>
</refentry>
<!--
Local Variables:
mode: xml
End:
-->

231
scripts/deb-reversion.sh Executable file
View file

@ -0,0 +1,231 @@
#!/bin/bash
#
# deb-reversion -- a script to bump a .deb file's version number.
#
# Copyright © martin f. krafft <madduck@madduck.net>
# with contributions by: Goswin von Brederlow, Filippo Giunchedi
# Released under the terms of the Artistic License 2.0
#
# TODO:
# - add debugging output.
# - allow to be used on dpkg-source and dpkg-deb unpacked source packages.
#
set -eu
PROGNAME=${0##*/}
PROGVERSION=0.9.1
VERSTR='LOCAL.'
versioninfo() {
echo "$PROGNAME $PROGVERSION"
echo "$PROGNAME is copyright © martin f. krafft"
echo "Released under the terms of the Artistic License 2.0"
echo "This programme is part of devscripts ###VERSION###."
}
usage() {
cat <<-_eousage
Usage: $PROGNAME [options] .deb-file [log message]
$PROGNAME -o <version> -c
Increase the .deb file's version number, noting the change in the
changelog with the specified log message. You should run this
program either as root or under fakeroot.
Options:
_eousage
cat <<-_eooptions | column -s\& -t
-v ver|--new-version=ver & use this as new version number
-o old|--old-version=ver & calculate new version number based on this old one
-c|--calculate-only & only calculate (and print) the augmented version
-s str|--string=str & append this string instead of '$VERSTR' to
& calculate new version number
-k script|--hook=script & call this script before repacking
-D|--debug & call dpkg-deb in debug mode
-b|--force-bad-version & passed through to dch
-h|--help & show this output
-V|--version & show version information
_eooptions
}
write() {
local PREFIX; PREFIX="$1"; shift
echo "${PREFIX}: $PROGNAME: $@" >&2
}
err() {
write E "$@"
}
CURDIR="$(pwd)"
SHORTOPTS=hVo:v:ck:Ds:b
LONGOPTS=help,version,old-version:,new-version:,calculate-only,hook:,debug,string:,force-bad-version
eval set -- "$(getopt -s bash -o $SHORTOPTS -l $LONGOPTS -n "$PROGNAME" -- "$@")"
CALCULATE=0
DPKGDEB_DEBUG=
DEB=
DCH_OPTIONS=
for opt in "$@"; do
case "${OPT_STATE:-}" in
SET_OLD_VERSION) OLD_VERSION="$opt";;
SET_NEW_VERSION) NEW_VERSION="$opt";;
SET_STRING) VERSTR="$opt";;
SET_HOOK) HOOK="$opt";;
*) :;;
esac
[ -n "${OPT_STATE:-}" ] && unset OPT_STATE && continue
case $opt in
-v|--new-version) OPT_STATE=SET_NEW_VERSION;;
-o|--old-version) OPT_STATE=SET_OLD_VERSION;;
-c|--calculate-only|--print-only) CALCULATE=1;;
-s|--string) OPT_STATE=SET_STRING;;
-k|--hook) OPT_STATE=SET_HOOK;;
-D|--debug) DPKGDEB_DEBUG=--debug;;
-b|--force-bad-version) DCH_OPTIONS="${DCH_OPTIONS} -b";;
-h|--help) usage; exit 0;;
-V|--version) versioninfo; exit 0;;
--) :;;
*)
if [ -f "$opt" ]; then
if [ -n "$DEB" ]; then
err "multiple .deb files specified: ${DEB##*/} and $opt"
exit 1
else
case "$opt" in
/*.deb|/*.udeb) DEB="$opt";;
*.deb| *.udeb) DEB="${CURDIR}/$opt";;
*)
err "not a .deb file: $opt";
exit 2
;;
esac
fi
else
LOG="${LOG:+$LOG }$opt"
fi
;;
esac
done
if [ $CALCULATE -eq 0 ] || [ -z "${OLD_VERSION:-}" ]; then
if [ -z "$DEB" ]; then
err no .deb file specified.
exit 3
fi
fi
if [ -n "${NEW_VERSION:-}" ] && [ $CALCULATE -eq 1 ]; then
echo "$PROGNAME error: the options -v and -c cannot be used together" >&2
usage
exit 4
fi
make_temp_dir() {
TMPDIR=$(mktemp -d --tmpdir deb-reversion.XXXXXX)
trap 'rm -rf "$TMPDIR"' EXIT
mkdir -p ${TMPDIR}/package
TMPDIR=${TMPDIR}/package
}
extract_deb_file() {
dpkg-deb $DPKGDEB_DEBUG --extract "$1" .
dpkg-deb $DPKGDEB_DEBUG --control "$1" DEBIAN
}
get_version() {
dpkg-deb -f "$1" Version
}
bump_version() {
case "$1" in
*${VERSTR}[0-9]*)
REV=${1##*${VERSTR}}
echo ${1%${VERSTR}*}${VERSTR}$((++REV));;
*-*)
echo ${1}${VERSTR}1;;
*)
echo ${1}-0${VERSTR}1;;
esac
}
call_hook() {
[ -z "${HOOK:-}" ] && return 0
export VERSION
sh -c "$HOOK"
}
change_version() {
PACKAGE=$(sed -ne 's,^Package: ,,p' DEBIAN/control)
VERSION=$1
# changelog massaging is only needed in the deb (not-udeb) case:
if [ "$DEB_TYPE" = "deb" ]; then
LOGFILE=
for i in changelog{,.Debian}.gz; do
[ -f usr/share/doc/${PACKAGE}/$i ] \
&& LOGFILE=usr/share/doc/${PACKAGE}/$i
done
if [ -n "$LOGFILE" ]; then
mkdir -p debian
zcat "$LOGFILE" > debian/changelog
shift
dch $DCH_OPTIONS -v "$VERSION" -- "$@"
call_hook
gzip -9 -c debian/changelog >| "$LOGFILE"
MD5SUM=$(md5sum "$LOGFILE")
sed -i "s@^[^ ]* $LOGFILE\$@$MD5SUM@" DEBIAN/md5sums
fi
else
call_hook
fi
sed -i -e "s,^Version: .*,Version: $VERSION," DEBIAN/control
rm -rf debian
}
repack_file() {
cd ..
dpkg-deb -b package >/dev/null
debfile=$(DPKG_COLORS=never DPKG_NLS=0 dpkg-name package.deb | sed -e "s,.*['\`]\(.*\).,\1,")
# if Package-Type: udeb is absent, dpkg-name can't rename into *.udeb,
# so we're left to an extra rename afterwards:
if [ "$DEB_TYPE" = udeb ]; then
udebfile=${debfile%%.deb}.udeb
mv $debfile $udebfile
echo $udebfile
else
echo $debfile
fi
}
[ -z "${OLD_VERSION:-}" ] && OLD_VERSION="$(get_version "$DEB")"
[ -z "${NEW_VERSION:-}" ] && NEW_VERSION="$(bump_version $OLD_VERSION)"
if [ $CALCULATE -eq 1 ]; then
echo $NEW_VERSION
exit 0
fi
if [ $(id -u) -ne 0 ]; then
err need root rights.
exit 5
fi
make_temp_dir
cd "$TMPDIR"
DEB_TYPE=$(echo "$DEB"|sed 's/.*[.]//')
extract_deb_file "$DEB"
change_version "$NEW_VERSION" "${LOG:-Bumped version with $PROGNAME}"
FILE="$(repack_file)"
if [ -f "$CURDIR/$FILE" ]; then
echo "$CURDIR/$FILE exists, moving to $CURDIR/$FILE.orig ." >&2
mv -i "$CURDIR/$FILE" "$CURDIR/$FILE.orig"
fi
mv "../$FILE" "$CURDIR"
echo "version $VERSION of $PACKAGE is now available in $FILE ." >&2

251
scripts/deb-why-removed.pl Executable file
View file

@ -0,0 +1,251 @@
#!/usr/bin/perl
#
# Copyright © 2017-2019 Guillem Jover <guillem@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use strict;
use warnings;
use File::Basename;
use File::Path qw(make_path);
use File::Copy qw(cp);
use File::Spec;
use Getopt::Long qw(:config posix_default no_ignorecase);
use HTTP::Tiny;
use Dpkg::Index;
use Devscripts::Output;
my $VERSION = '0.0';
my ($PROGNAME) = $0 =~ m{(?:.*/)?([^/]*)};
my %url_map = ('debian' => 'https://ftp-master.debian.org/removals-full.822');
my $default_url_origin = 'debian';
#
# Functions
#
sub version {
print "$PROGNAME $VERSION (devscripts ###VERSION###)\n";
}
sub usage {
print <<HELP;
Usage: $PROGNAME [<option>...] <package>...
Options:
-u, --url URL URL to the removals deb822 file list (defaults to
<$url_map{$default_url_origin}>).
--no-refresh Do not refresh the cached removals file even if old.
-h, -?, --help Print this help text.
--version Print the version.
HELP
}
# XXX: DAK produces broken output, fix it up here before we process it.
#
# The two current bogus instances are, at least two fused paragraphs, and
# bogus "sh: 0: getcwd() failed: No such file or directory" command output
# interpersed within the file.
sub fixup_broken_metadata {
my $cachefile = shift;
my $para_sep = 1;
open my $fh_old, '<', $cachefile
or ds_error("cannot open cache file $cachefile for fixup");
open my $fh_new, '>', "$cachefile.new"
or ds_error("cannot open cache file $cachefile.new for fixup");
while (my $line = <$fh_old>) {
if ($line =~ m/^\s*$/) {
$para_sep = 1;
} elsif (not $para_sep and $line =~ m/^Date:/) {
# XXX: We assume each paragraph starts with a Date: field, and
# inject the missing newline.
print {$fh_new} "\n";
} else {
$para_sep = 0;
}
# XXX: Fixup shell output detritus.
if ($line =~ s/sh: 0: getcwd\(\) failed: No such file or directory//) {
# Remove the trailing line so that the next line gets folded back
# into this one.
chomp $line;
}
print {$fh_new} $line;
}
close $fh_new or ds_error("cannot write cache file $cachefile.new");
close $fh_old;
# Preserve the original mtime so that mirroring works.
my ($atime, $mtime) = (stat $cachefile)[8, 9];
utime $atime, $mtime, "$cachefile.new";
rename "$cachefile.new", $cachefile
or ds_error("cannot replace cache file with fixup version");
}
sub cache_file {
my ($url, $cachefile) = @_;
cp($url, $cachefile) or ds_error("cannot copy removal metadata: $!");
fixup_broken_metadata($cachefile);
}
sub cache_http {
my ($url, $cachefile) = @_;
my $http = HTTP::Tiny->new(verify_SSL => 1);
my $resp = $http->mirror($url, $cachefile);
unless ($resp->{success}) {
ds_error(
"cannot fetch removal metadata: $resp->{status} $resp->{reason}");
}
if ($resp->{status} != 304) {
fixup_broken_metadata($cachefile);
}
}
#
# Main program
#
my $opts;
GetOptions(
'url|u=s' => \$opts->{'url'},
'no-refresh' => \$opts->{'no-refresh'},
'help|h|?' => sub { usage(); exit 0 },
'version' => sub { version(); exit 0 },
)
or die "\nUsage: $PROGNAME [<option>...] <package>...\n"
. "Run $PROGNAME --help for more details.\n";
unless (@ARGV) {
ds_error('need at least one package name as an argument');
}
my $url = $opts->{url} // $default_url_origin;
$url = $url_map{$url} if $url_map{$url};
my $cachehome = $ENV{XDG_CACHE_HOME};
$cachehome ||= File::Spec->catdir($ENV{HOME}, '.cache') if length $ENV{HOME};
if (length $cachehome == 0) {
ds_error("unknown user home, cannot download removal metadata");
}
my $cachedir = File::Spec->catdir($cachehome, 'devscripts', 'deb-why-removed');
my $cachefile = File::Spec->catfile($cachedir, basename($url));
if (not -d $cachedir) {
make_path($cachedir);
}
if (not -e $cachefile or (-e _ and not $opts->{'no-refresh'})) {
# Normalize the URL.
$url =~ s{^file://}{};
# Cache the file locally.
if (-e $url) {
cache_file($url, $cachefile);
} else {
cache_http($url, $cachefile);
}
}
my $meta
= Dpkg::Index->new(
get_key_func => sub { return $_[0]->{Sources} // $_[0]->{Binaries} // '' },
);
$meta->load($cachefile, compression => 0);
STANZA: foreach my $entry ($meta->get) {
foreach my $pkg (@ARGV) {
# XXX: Skip bogus entries with no indexable fields.
next
if not defined $entry->{Sources}
and not defined $entry->{Binaries};
next
if ($entry->{Sources} // '') !~ m/\Q$pkg\E_/
&& ($entry->{Binaries} // '') !~ m/\Q$pkg\E_/;
print $entry->output();
print "\n";
next STANZA;
}
}
=encoding utf8
=head1 NAME
deb-why-removed - shows the reason a package was removed from the archive
=head1 SYNOPSIS
B<deb-why-removed> [I<option>...] I<package>...
=head1 DESCRIPTION
This program will download the removals metadata from the archive, search
and print the entries within for a source or binary package name match.
=head1 OPTIONS
=over 4
=item B<-u>, B<--url> I<URL>
URL to the archive removals deb822-formatted file list.
This can be either an actual URL (https://, http://, file://), an pathname
or an origin name.
Currently the only origin name known is B<debian>.
=item B<--no-refresh>
Do not refresh the cached removals file even if there is a newer version
in the archive.
=item B<-h>, B<-?>, B<--help>
Show a help message and exit.
=item B<--version>
Show the program version.
=back
=head1 FILES
=over 4
=item I<cachedir>B</devscripts/deb-why-removed/>
This directory contains the cached removal files downloaded from the archive.
I<cachedir> will be either B<$XDG_CACHE_HOME> or if that is not defined
B<$HOME/.cache/>.
=back
=head1 SEE ALSO
L<https://ftp-master.debian.org/#removed>
=cut

141
scripts/deb2apptainer.1 Normal file
View file

@ -0,0 +1,141 @@
.TH "DEB2APPTAINER" "1" "February 2024" "" ""
.hy
.SH NAME
.PP
\f[B]deb2apptainer\f[R] - Build a Singularity/Apptainer image with given
Debian packages
.SH SYNOPSIS
.PP
\f[B]deb2apptainer\f[R] [\f[B]-hB\f[R]][\f[B]-c\f[R] \f[I]CMD\f[R]]
[\f[B]-f\f[R] \f[I]FROM\f[R]][\f[B]-n\f[R] \f[I]NAME\f[R]]
[\f[B]-o\f[R] \f[I]DIR\f[R]][\f[B]-p\f[R] \f[I]PRE_SCRIPT\f[R]]
[\f[B]-s\f[R] \f[I]POST_SCRIPT\f[R]] \f[I]packages\f[R]
.br
\f[B]deb2singularity\f[R] [\f[B]-hB\f[R]][\f[B]-c\f[R] \f[I]CMD\f[R]]
[\f[B]-f\f[R] \f[I]FROM\f[R]][\f[B]-n\f[R] \f[I]NAME\f[R]]
[\f[B]-o\f[R] \f[I]DIR\f[R]][\f[B]-p\f[R] \f[I]PRE_SCRIPT\f[R]]
[\f[B]-s\f[R] \f[I]POST_SCRIPT\f[R]] \f[I]packages\f[R]
.SH DESCRIPTION
.PP
\f[B]deb2apptainer\f[R] is a simple script which takes as input a list
of Debian packages and generates automatically a Singularity/Apptainer
container including these packages.
A set of \f[I]freedesktop.org\f[R] desktop launchers are also generated
based on the .desktop and icon files found in the packages.
In addition, a desktop launcher is created to start the container in a
Terminal.
.PP
This tool is suited for deploying applications as containers, as well as
for testing Debian packages in a sandbox.
.SH OPTIONS
.TP
\f[B]-B\f[R]
do NOT build the image (default is to build).
A \f[I]build\f[R] script is generated in the DIR target directory.
.TP
\f[B]-c EXEC\f[R]
Command to run in the container (default to \f[I]/bin/bash\f[R]).
.TP
\f[B]-f FROM\f[R]
Distribution is to be used (default to \f[I]debian:stable\f[R]).
.TP
\f[B]-h\f[R]
Show this help
.TP
\f[B]-n NAME\f[R]
Name of the image (default is built from the package list).
.TP
\f[B]-o DIR\f[R]
Use given directory DIR for the build (default is in /tmp).
.TP
\f[B]-p PRE_SCRIPT\f[R]
Execute the given script \f[I]PRE_SCRIPT\f[R] before packages install.
.TP
\f[B]-s POST_SCRIPT\f[R]
Execute the given script \f[I]POST_SCRIPT\f[R] after packages install.
.TP
\f[B]packages\f[R]
The package list can be any Debian package, as well as local .deb files.
.SH FILES
.IP \[bu] 2
DIR/README
.IP \[bu] 2
DIR/image.def
.IP \[bu] 2
DIR/image.sif
.IP \[bu] 2
DIR/launchers/
.IP \[bu] 2
DIR/icons/
.IP \[bu] 2
DIR/build
.IP \[bu] 2
DIR/start
.SH NOTES
.PP
You obviously require to have \f[I]apptainer\f[R] installed.
.PP
Get the Debian package at: -
https://apptainer.org/docs/admin/main/installation.html#install-debian-packages
.PP
Usual commands typically used to handle Apptainer/Singularity containers
are:
.TP
\f[B]build\f[R]
apptainer build image.sif
.TP
\f[B]run\f[R]
apptainer run image.sif apptainer run \[en]nv image.sif # with NVIDIA
GPU pass-through
.TP
\f[B]info\f[R]
apptainer inspect image.sif
.TP
\f[B]header\f[R]
apptainer sif header image.sif
.TP
\f[B]data\f[R]
apptainer sif list image.sif
.SH EXAMPLES
.TP
Create a Singularity/Apptainer container with package \f[I]x11-apps\f[R] in directory \f[I]/tmp/xeyes\f[R], and launch \f[I]xeyes\f[R]:
.IP \[bu] 2
deb2apptainer -o /tmp/xeyes x11-apps
.IP \[bu] 2
/tmp/xeyes/start xeyes
.RS
.PP
A Desktop launcher is created as
/tmp/xeyes/launchers/x11-apps-terminal.desktop
.RE
.TP
Create a Singularity/Apptainer container with \f[I]x11-apps\f[R] and \f[I]meshlab\f[R]
deb2apptainer x11-apps meshlab
.TP
Create a Singularity/Apptainer container making sure software channels are active:
.IP \[bu] 2
echo \[lq]sed -i `s/main/main contrib non-free/g'
/etc/apt/sources.list\[rq] > pre.sh
.IP \[bu] 2
deb2apptainer -p pre.sh x11-apps
.TP
Create a Singularity/Apptainer container based on specific Debian version, and make use of the GPU:
.IP \[bu] 2
echo \[lq]echo `deb http://deb.debian.org/debian bullseye main contrib
non-free' >> /etc/apt/sources.list\[rq] > pre-script.sh
.IP \[bu] 2
deb2apptainer -n pyhst2 -f debian:bullseye -p pre-script.sh -o
/tmp/apptainer-pyhst2/ python3-pyhst2-cuda nvidia-smi
nvidia-cuda-toolkit
.IP \[bu] 2
apptainer run \[en]nv /tmp/apptainer-pyhst2/pyhst2.sif nvidia-smi
\[ga]\[ga]\[ga]
.SH AUTHORS
.PP
Emmanuel Farhi (emmanuel.farhi\[at]synchrotron-soleil.fr)
.SH SEE ALSO
.PP
deb2docker(1), distrobox-create(1), distrobox-enter(1), docker(1),
apptainer(1)
.SH AUTHORS
Emmanuel Farhi.

382
scripts/deb2apptainer.sh Executable file
View file

@ -0,0 +1,382 @@
#! /bin/bash
# Purpose: install Debian packages in a Singularity/Apptainer image
#
# Usage: deb2apptainer [options] packages
# -B do NOT build the image (default is to build)
# -c CMD command to run in the container (default to '/bin/bash')
# -f FROM indicate which distribution is to be used (default to debian:stable)
# -h show this help
# -n NAME name of the image (default to package list)
# -o DIR use given directory for the build (default in /tmp)
# -p PRE execute the given script during the container build (before packages install)
# -s POST execute the given script during the container build (after packages install)
# -v show package version
# The package list can be any Debian package, as well as local .deb
#
# Example: 'deb2apptainer -o /tmp/xeyes x11-apps' then '/tmp/xeyes/start xeyes'
# (c) E. Farhi - Synchrotron SOLEIL - GPL3
# requires:
# bash
# apptainer.deb from https://apptainer.org/docs/admin/main/installation.html#install-debian-packages
# info: apptainer inspect image.sif
# header: apptainer sif header image.sif
# data: apptainer sif list image.sif
set -e
# default settings -------------------------------------------------------------
FROM=debian:stable
BUILD=1
NAME=
SETNAME=0
CMD="/bin/bash"
WORK_DIR=
SCRIPT=
PRE=
VERSION=1.0.8
# handle input arguments -------------------------------------------------------
while getopts vhBf:n:c:o:d:p:s: flag
do
case "${flag}" in
h) # display help
echo "Usage: $0 [-hB][-c CMD][-f FROM][-n NAME][-o DIR][-s SCRIPT] packages..."
echo " Build a Singularity/Apptainer image with given Debian packages."
echo " Options:"
echo " -B do NOT build the image (default is to build)"
echo " -c EXEC command to run in the container (default to $CMD)"
echo " -f FROM indicate which distribution is to be used (default to $FROM)"
echo " -h show this help"
echo " -n NAME name of the image (default to package list)"
echo " -o DIR use given directory for the build (default is in /tmp)"
echo " -p PRE execute script PRE before packages install"
echo " -s POST execute script POST after packages install"
echo " -v show package version"
echo " The package list can be any Debian package, as well as local .deb"
echo " "
echo "Example: '$0 -o /tmp/xeyes x11-apps' then '/tmp/xeyes/start xeyes'"
exit 0;
;;
B) # do not build the image
BUILD=0
;;
f) # set FROM image
FROM=${OPTARG}
;;
n) # set image name
NAME=${OPTARG}
;;
c) # command to execute
CMD=${OPTARG}
;;
o|d) # output directory
WORK_DIR=${OPTARG}
;;
p) # PRE SCRIPT
PRE=${OPTARG}
;;
s) # SCRIPT (POST)
SCRIPT=${OPTARG}
;;
v) # VERSION
echo "$0 version $VERSION"
exit 0;
;;
*)
echo "ERROR: Invalid option. Use -h for help."
exit 1;
;;
esac
done
shift $((OPTIND-1))
# check that apptainer is installed
if ! command -v apptainer > /dev/null
then
echo "ERROR: apptainer could not be found. Install it from"
echo " https://apptainer.org/docs/admin/main/installation.html#install-debian-packages"
exit 1
fi
# set name when not set
if [ "x$NAME" = "x" ]; then
SETNAME=1
fi
# create a temporary directory to work in --------------------------------------
if [ "x$WORK_DIR" = "x" ]; then
N=`basename $0`
WORK_DIR=`mktemp -p /tmp -d $N-XXXXXXXX`
else
mkdir -p $WORK_DIR || echo "ERROR: Invalid directory $WORK_DIR"
fi
PW=$PWD
# search for executable commands and launchers in the packages -----------------
DEB=
# get a local copy of packages and find bin and desktop files
mkdir -p $WORK_DIR/apt || exit # hold deb pkg copies for analysis
echo "$0: creating image $NAME in $WORK_DIR"
echo "Getting Debian packages..."
for i in $@; do
echo " $i"
if [ -f "$i" ]; then
cp $i $WORK_DIR/apt/
n=`basename $i`
DEB="$DEB /opt/install/apt/$n"
else
DEB="$DEB $i"
cd $WORK_DIR/apt
apt download $i
cd $PW
fi
done
echo " " >> $WORK_DIR/README
echo "Created with $0" >> $WORK_DIR/README
echo "$ARGS" >> $WORK_DIR/README
echo " " >> $WORK_DIR/file_list.txt
for i in $WORK_DIR/apt/*.deb; do
echo "Analyzing $i"
N=$(dpkg-deb -f $i Package) || continue
# set the container name if needed
if [ "x$SETNAME" = "x2" ]; then
NAME="$NAME-$N"
fi
if [ "x$SETNAME" = "x1" ]; then
SETNAME=2
NAME=$N
fi
echo "Package $N ------------------------------------------" >> $WORK_DIR/README
dpkg-deb -I $i >> $WORK_DIR/README
F=`dpkg -c $i`
echo "$F" >> $WORK_DIR/file_list.txt
echo " " >> $WORK_DIR/README
done
# prepare the Singularity definition file --------------------------------------
FILE="$WORK_DIR/$NAME.def"
DATE=`date`
# get a random UUID for /etc/machine-id
# see:
# - https://github.com/denisbrodbeck/machineid/issues/5
# - https://github.com/apptainer/singularity/issues/3609
if command -v uuidgen > /dev/null
then
UUID=`uuidgen | sed "s/-//g"`
UUID="echo $UUID > /etc/machine-id"
else
if [ -f "/etc/machine-id" ]; then
UUID=`cat /etc/machine-id`
UUID="echo $UUID > /etc/machine-id"
else
UUID=0de1bbc1982243198b320e756d12224b
fi
fi
# the base command to start the containers from image
cmd="apptainer run $NAME.sif "
if [ "x$CMD" = "x/bin/sh" -o "x$CMD" = "x/bin/bash" ]; then
cmd_arg="-c"
else
cmd_arg=""
fi
if [ -f "$PRE" ]; then
cp $PRE $WORK_DIR/
N=`basename $PRE`
PRE="chmod a+x /opt/install/$N && sh -c \"/opt/install/$N\""
PRE_FILE="$N /opt/install/"
else
PRE_FILE=
fi
if [ -f "$SCRIPT" ]; then
cp $SCRIPT $WORK_DIR/
N=`basename $SCRIPT`
SCRIPT="chmod a+x /opt/install/$N && sh -c \"/opt/install/$N\""
SCRIPT_FILE="$N /opt/install/"
else
SCRIPT_FILE=
fi
echo "Creating Singularity definition file $NAME into $FILE"
dd status=none of=${FILE} << EOF
# created by $0 on $DATE
#
# Singularity/Apptainer image $NAME
#
# build: apptainer build $NAME.sif
# run: apptainer run $NAME.sif
Bootstrap: docker
From: $FROM
%files
apt/ /opt/install/
README /opt/install/
file_list.txt /opt/install/
$NAME.def /opt/install/
$PRE_FILE
$SCRIPT_FILE
%post
$PRE
apt-get update -y
apt-get install -y --no-install-recommends bash $DEB
$UUID
$SCRIPT
rm -rf /opt/install/apt
useradd -ms /bin/bash user
%environment
export LC_ALL=C
export PATH=/usr/games:$PATH
%runscript
echo "Starting container $NAME, built on $DATE from $FROM"
cat /opt/install/README || echo " "
if [ \$# -ge 1 ]; then $CMD $cmd_arg \$@; else $CMD; fi
%help
This is a $NAME Singularity/Apptainer with packages $@.
The default start-up command is $CMD.
Installation files and README are in /opt/install
%labels
Name $NAME
System $FROM
Date $DATE
Creator $0
Command $CMD
EOF
# build image ------------------------------------------------------------------
FILE=$WORK_DIR/build
dd status=none of=${FILE} << EOF
#!/bin/bash
# created by $0 on $DATE
# $ARGS
#
# build image $NAME with
#
# Usage: build
apptainer build $NAME.sif $NAME.def
# handle of Desktop launchers
mkdir -p launchers/
mkdir -p icons/
# get .desktop files ----------------------------------------------------------
D=\$(grep '\.desktop' file_list.txt) || echo "WARNING: No desktop file found."
# get the desktop files
D=\$(echo "\$D" | awk '{ print \$6 }')
# we need to copy them back, as well as their icons, and change the Exec lines
for i in \$D; do
if [ \${i:0:1} == "." ] ; then
i=\$(echo "\$i" | cut -c 2-)
fi
n=\`basename \$i\`
apptainer exec $NAME.sif cat \$i >> launchers/\$n || echo "WARNING: Failed to get desktop launcher \$i"
done
# get icon files --------------------------------------------------------------
D=\$(grep 'icon' file_list.txt) || echo "WARNING: No icon file found."
# get the icon files
D=\$(echo "\$D" | awk '{ print \$6 }')
# we need to copy the icon files back
for i in \$D; do
if [ \${i:0:1} == "." ] ; then
i=\$(echo "\$i" | cut -c 2-)
fi
n=\`basename \$i\`
apptainer exec $NAME.sif cp \$i /tmp/$n &> /dev/null || n=
if [ -f "/tmp/\$n" ]; then
mv /tmp/\$n icons/\$n
fi
done
# adapt the Desktop launchers to insert 'run', set Icons=
for i in launchers/*; do
if [ ! -f "\$i" ]; then continue; fi
I=\$(grep 'Icon=' \$i | cut -d = -f 2) || I=
if [ ! -z "\$I" ]; then
n=\`basename \$I\`
if [ ! -f "icons/\$n" ]; then
# get closest file that match Icon name when initial name is not present as a file
n1=( $icons/\$n* ) || n1=
if [ ! -z "\$n1" ]; then
n=\`basename \${n1[0]}\`
fi
fi
sed -i "s|Icon=.*|Icon=icons/\$n|g" \$i || echo " "
fi
sed -i 's|Exec=|&$cmd $cmd_arg |g' \$i || echo " "
sed -i 's|Terminal=false|Terminal=true|g' \$i || echo " "
chmod a+x \$i || echo " "
done
# create a Terminal launcher
echo "[Desktop Entry]" > launchers/$NAME-terminal.desktop
echo "Type=Application" >> launchers/$NAME-terminal.desktop
echo "Name=$NAME Terminal" >> launchers/$NAME-terminal.desktop
echo "Terminal=true" >> launchers/$NAME-terminal.desktop
echo "Exec=$cmd" >> launchers/$NAME-terminal.desktop
chmod a+x launchers/$NAME-terminal.desktop
EOF
chmod a+x $WORK_DIR/build
if [ "x$BUILD" = "x1" ]; then
# build the image
(cd $WORK_DIR && ./build)
chmod a+x $WORK_DIR/$NAME.sif || exit
else
echo "INFO: To build this image, use: cd $WORK_DIR; ./build"
echo " "
cat $WORK_DIR/build
fi
# ------------------------------------------------------------------------------
# get executables and Desktop launchers (from the container)
echo "------------------------------------------------------" >> $WORK_DIR/README
B=$(grep '\.desktop' $WORK_DIR/file_list.txt) || echo " "
echo "$B" >> $WORK_DIR/README
FILE=$WORK_DIR/start
dd status=none of=${FILE} << EOF
#!/bin/bash
# created by $0 on $DATE
# $ARGS
#
# start a container from image $NAME
#
# Usage: start [CMD]
# default CMD is $CMD
$cmd \$@
EOF
chmod a+x $WORK_DIR/start
# display final message
echo "--------------------------------------------"
echo "The image $NAME has been prepared in $WORK_DIR"
echo " Desktop launchers are available in $WORK_DIR/launchers"
echo "To start $NAME, use any of: "
echo " cd $WORK_DIR; ./$NAME.sif [cmd]"
echo " cd $WORK_DIR; ./start [cmd]"
echo " "

111
scripts/deb2docker.1 Normal file
View file

@ -0,0 +1,111 @@
.TH "DEB2DOCKER" "1" "February 2024" "" ""
.hy
.SH NAME
.PP
\f[B]deb2docker\f[R] - Build a Docker image with given Debian packages
.SH SYNOPSIS
.PP
\f[B]deb2docker\f[R] [-hB][-c CMD][-f FROM][-n NAME][-o DIR][-p
PRE_SCRIPT][-s POST_SCRIPT] \f[I]packages\f[R]
.SH DESCRIPTION
.PP
\f[B]deb2docker\f[R] is a simple script which takes as input a list of
Debian packages and generates automatically a Docker container including
these packages.
A set of \f[I]freedesktop.org\f[R] desktop launchers are also generated
based on the .desktop and icon files found in the packages.
In addition, a desktop launcher is created to start the container in a
Terminal.
.PP
This tool is suited for deploying applications as containers, as well as
for testing Debian packages in a sandbox.
.SH OPTIONS
.TP
\f[B]-B\f[R]
do NOT build the image (default is to build).
A \f[I]build\f[R] script is generated in the DIR target directory.
.TP
\f[B]-c EXEC\f[R]
Command to run in the container (default to \f[I]/bin/bash\f[R]).
.TP
\f[B]-f FROM\f[R]
Distribution is to be used (default to \f[I]debian:stable\f[R]).
.TP
\f[B]-h\f[R]
Show this help
.TP
\f[B]-n NAME\f[R]
Name of the image (default is built from the package list).
.TP
\f[B]-o DIR\f[R]
Use given directory DIR for the build (default is in /tmp).
.TP
\f[B]-p PRE_SCRIPT\f[R]
Execute the given script \f[I]PRE_SCRIPT\f[R] before packages install.
.TP
\f[B]-s POST_SCRIPT\f[R]
Execute the given script \f[I]POST_SCRIPT\f[R] after packages install.
.TP
\f[B]packages\f[R]
The package list can be any Debian package, as well as local .deb files.
.SH FILES
.IP \[bu] 2
DIR/README
.IP \[bu] 2
DIR/Dockerfile
.IP \[bu] 2
DIR/launchers/
.IP \[bu] 2
DIR/icons/
.IP \[bu] 2
DIR/build
.IP \[bu] 2
DIR/start
.SH NOTES
.TP
You need of course to have Docker installed and be part of the \f[I]docker\f[R] group:
.IP \[bu] 2
sudo apt install docker.io
.IP \[bu] 2
sudo usermod -aG docker $USER\[ga]
.PP
You may have to manually configure Docker to pass a proxy configuration.
.PP
Usual commands typically used to handle Docker containers are:
.TP
\f[B]build\f[R]
docker build \[en]rm Dockerfile \f[B]run\f[R]
docker run \[en]rm -it NAME \f[B]clean\f[R]
docker rmi NAME \f[B]clean ALL\f[R]
docker system prune -a
.SH EXAMPLES
.TP
Create a Docker container with package \f[I]x11-apps\f[R] in directory \f[I]/tmp/xeyes\f[R], and launch \f[I]xeyes\f[R]:
.IP \[bu] 2
deb2docker -o /tmp/xeyes x11-apps
.IP \[bu] 2
/tmp/xeyes/start xeyes
.RS
.PP
A Desktop launcher is created as
/tmp/xeyes/launchers/x11-apps-terminal.desktop
.RE
.TP
Create a Docker container with \f[I]x11-apps\f[R] and \f[I]meshlab\f[R]
deb2docker x11-apps meshlab
.TP
Create a Docker container making sure software channels are active:
.IP \[bu] 2
echo \[lq]sed -i `s/main/main contrib non-free/g'
/etc/apt/sources.list\[rq] > pre.sh
.IP \[bu] 2
deb2docker -p pre.sh x11-apps
.SH AUTHORS
.PP
Emmanuel Farhi (emmanuel.farhi\[at]synchrotron-soleil.fr)
.SH SEE ALSO
.PP
deb2apptainer(1), distrobox-create(1), distrobox-enter(1), docker(1),
apptainer(1)
.SH AUTHORS
Emmanuel Farhi.

349
scripts/deb2docker.sh Executable file
View file

@ -0,0 +1,349 @@
#! /bin/bash
# Purpose: install Debian packages in a Docker image
#
# Usage: deb2docker [options] packages
# -B do NOT build the image (default is to build)
# -c CMD command to run in the container (default to '/bin/bash')
# -f FROM indicate which distribution is to be used (default to debian:stable)
# -h show this help
# -n NAME name of the image (default to package list)
# -o DIR use given directory for the build (default in /tmp)
# -p PRE execute the given script during the container build (before packages install)
# -s POST execute the given script during the container build (after packages install)
# -v show package version
# The package list can be any Debian package, as well as local .deb
#
# Example: 'deb2docker -o /tmp/xeyes x11-apps' then '/tmp/xeyes/start xeyes'
# build: docker build --rm Dockerfile
# run: docker run --rm -it NAME
# clean: docker rmi NAME
# clean ALL: docker system prune -a
# (c) E. Farhi - Synchrotron SOLEIL - GPL3
# requires:
# bash
# docker.io
# requires: docker privileges:
# sudo usermod -aG docker $USER
set -e
# default settings -------------------------------------------------------------
FROM=debian:stable
BUILD=1
NAME=
SETNAME=0
CMD="/bin/bash"
WORK_DIR=
SCRIPT=
PRE=
VERSION=1.0.8
ARGS="$@"
# handle input arguments -------------------------------------------------------
while getopts vhBf:n:c:o:d:p:s: flag
do
case "${flag}" in
h) # display help
echo "Usage: $0 [-hB][-c CMD][-f FROM][-n NAME][-o DIR][-s SCRIPT] packages..."
echo " Build a Docker image with given Debian packages."
echo " Options:"
echo " -B do NOT build the image (default is to build)"
echo " -c EXEC command to run in the container (default to $CMD)"
echo " -f FROM indicate which distribution is to be used (default to $FROM)"
echo " -h show this help"
echo " -n NAME name of the image (default to package list)"
echo " -o DIR use given directory for the build (default is in /tmp)"
echo " -p PRE execute script PRE before packages install"
echo " -s POST execute script POST after packages install"
echo " -v show package version"
echo " The package list can be any Debian package, as well as local .deb"
echo " "
echo "Example: '$0 -o /tmp/xeyes x11-apps' then '/tmp/xeyes/start xeyes'"
exit 0;
;;
B) # do not build the image
BUILD=0
;;
f) # set FROM image
FROM=${OPTARG}
;;
n) # set image name
NAME=${OPTARG}
;;
c) # command to execute
CMD=${OPTARG}
;;
o|d) # output directory
WORK_DIR=${OPTARG}
;;
p) # PRE SCRIPT
PRE=${OPTARG}
;;
s) # SCRIPT (POST)
SCRIPT=${OPTARG}
;;
v) # VERSION
echo "$0 version $VERSION"
exit 0;
;;
*)
echo "ERROR: Invalid option. Use -h for help."
exit 1;
;;
esac
done
shift $((OPTIND-1))
# check that docker is installed
if ! command -v docker > /dev/null
then
echo "ERROR: docker could not be found. Install it with: apt install docker.io"
exit 1
fi
# set name when not set
if [ "x$NAME" = "x" ]; then
SETNAME=1
fi
# create a temporary directory to work in --------------------------------------
if [ "x$WORK_DIR" = "x" ]; then
N=`basename $0`
WORK_DIR=`mktemp -p /tmp -d $N-XXXXXXXX`
else
mkdir -p $WORK_DIR || echo "ERROR: Invalid directory $WORK_DIR"
fi
PW=$PWD
# search for executable commands -----------------
DEB=
# get a local copy of packages and find bin and desktop files
mkdir -p $WORK_DIR/apt || exit # hold deb pkg copies for analysis
echo "$0: creating image $NAME in $WORK_DIR"
echo "Getting Debian packages..."
for i in $@; do
echo " $i"
if [ -f "$i" ]; then
cp $i $WORK_DIR/apt/
n=`basename $i`
DEB="$DEB /opt/install/$n"
else
DEB="$DEB $i"
cd $WORK_DIR/apt
apt download $i
cd $PW
fi
done
echo " " >> $WORK_DIR/README
echo "Created with $0" >> $WORK_DIR/README
echo "$ARGS" >> $WORK_DIR/README
echo " " >> $WORK_DIR/file_list.txt
for i in $WORK_DIR/apt/*.deb; do
echo "Analyzing $i"
N=$(dpkg-deb -f $i Package) || continue
# set the container name if needed
if [ "x$SETNAME" = "x2" ]; then
NAME="$NAME-$N"
fi
if [ "x$SETNAME" = "x1" ]; then
SETNAME=2
NAME=$N
fi
echo "Package $N ------------------------------------------" >> $WORK_DIR/README
dpkg-deb -I $i >> $WORK_DIR/README
F=`dpkg -c $i`
echo "$F" >> $WORK_DIR/file_list.txt
echo " " >> $WORK_DIR/README
done
# the base command to start the containers from image
cmd="docker run -it --net=host --env=DISPLAY --env='QT_X11_NO_MITSHM=1' --volume=\$HOME/.Xauthority:/home/user/.Xauthority:rw $NAME"
# prepare the Dockerfile -------------------------------------------------------
FILE="$WORK_DIR/Dockerfile"
DATE=`date`
if [ -f "$PRE" ]; then
cp $PRE $WORK_DIR/
N=`basename $PRE`
PRE="RUN chmod a+x /opt/install/$N && sh -c \"/opt/install/$N\""
PRE_FILE="ADD $N /opt/install/"
else
PRE_FILE=
fi
if [ -f "$SCRIPT" ]; then
cp $SCRIPT $WORK_DIR/
N=`basename $SCRIPT`
SCRIPT="RUN chmod a+x /opt/install/$N && sh -c \"/opt/install/$N\""
SCRIPT_FILE="ADD $N /opt/install/"
else
SCRIPT_FILE=
fi
echo "Creating Dockerfile $NAME into $FILE"
dd status=none of=${FILE} << EOF
# created by $0 on $DATE
# $ARGS
#
# Docker image $NAME
#
# build: docker build --rm Dockerfile
# run: docker run --rm -it $NAME
# clean: docker rmi $NAME
# clean ALL: docker system prune -a
FROM $FROM
# copy required packages
ADD apt/ /opt/install/
ADD README /opt/install/
ADD file_list.txt /opt/install/
ADD Dockerfile /opt/install/
$PRE_FILE
$SCRIPT_FILE
# execute/install
$PRE
RUN apt-get update -y
RUN apt-get install -y --no-install-recommends bash xauth $DEB
$SCRIPT
RUN rm /opt/install/*.deb || echo " "
# start the container (interactive/terminal)
RUN useradd -ms /bin/bash user
ENV DISPLAY :0
USER user
CMD ["$CMD"]
EOF
# build docker -----------------------------------------------------------------
FILE=$WORK_DIR/build
dd status=none of=${FILE} << EOF
#!/bin/bash
# created by $0 on $DATE
# $ARGS
#
# build image $NAME
#
# Usage: build
docker build --rm -t $NAME .
# handle of Desktop launchers
mkdir -p launchers/
mkdir -p icons/
# get .desktop files ----------------------------------------------------------
D=\$(grep '\.desktop' file_list.txt) || echo "WARNING: No desktop files found."
# get the desktop files
D=\$(echo "\$D" | awk '{ print \$6 }')
# we need to copy them back, as well as their icons, and change the Exec lines
# create a container from image to access the files
id=\$(docker create $NAME)
for i in \$D; do
docker cp \$id:\$i launchers/ || echo "WARNING: Failed to get desktop launcher \$i"
done
# get icon files --------------------------------------------------------------
D=\$(grep 'icon' file_list.txt) || echo "WARNING: No icon files found."
# get the icon files
D=\$(echo "\$D" | awk '{ print \$6 }')
# we need to copy them back, as well as their icons, and change the Exec lines
# create a container from image to access the files
id=\$(docker create $NAME)
for i in \$D; do
docker cp \$id:\$i icons/ &> /dev/null|| echo "WARNING: Failed to get icon \$i"
done
# cleanup
docker rm -v \$id
# adapt the Desktop launchers to insert 'run', set Icons=
for i in launchers/*; do
if [ ! -f "\$i" ]; then continue; fi
I=\$(grep 'Icon=' \$i | cut -d = -f 2) || I=
if [ ! -z "\$I" ]; then
n=\`basename \$I\`
if [ ! -f "icons/\$n" ]; then
# get closest file that match Icon name when initial name is not present as a file
n1=( icons/\$n* ) || n1=
if [ ! -z "\$n1" ]; then
n=\`basename \${n1[0]}\`
fi
fi
sed -i "s|Icon=.*|Icon=icons/\$n|g" \$i || echo " "
fi
I=\$(grep 'Exec=' \$i | cut -d = -f 2-) || I=
sed -i "s|Exec=.*|Exec=sh -c \"echo '$NAME'; $cmd \$I\"|g" \$i || echo " "
# make sure terminal is set (else 'docker -it' fails)
I=\$(grep 'Terminal=' \$i | cut -d = -f 2) || I=
if [ ! -z "\$I" ]; then
sed -i 's|Terminal=.*|Terminal=true|g' \$i || echo " "
else
echo "Terminal=true" >> \$i
fi
chmod a+x \$i || echo " "
done
# create a Terminal launcher
echo "[Desktop Entry]" > launchers/$NAME-terminal.desktop
echo "Type=Application" >> launchers/$NAME-terminal.desktop
echo "Name=$NAME Terminal" >> launchers/$NAME-terminal.desktop
echo "Terminal=true" >> launchers/$NAME-terminal.desktop
echo "Exec=sh -c \"echo '$NAME'; $cmd\"" >> launchers/$NAME-terminal.desktop
chmod a+x launchers/$NAME-terminal.desktop
EOF
chmod a+x $WORK_DIR/build
if [ "x$BUILD" = "x1" ]; then
# build the image
(cd $WORK_DIR && ./build)
else
echo "INFO: To build this image, use: cd $WORK_DIR; ./build"
echo " "
cat $WORK_DIR/build
fi
# ------------------------------------------------------------------------------
# get executables and Desktop launchers (from the Docker)
echo "------------------------------------------------------" >> $WORK_DIR/README
B=$(grep '\.desktop' $WORK_DIR/file_list.txt) || echo " "
echo "$B" >> $WORK_DIR/README
FILE=$WORK_DIR/start
dd status=none of=${FILE} << EOF
#!/bin/bash
# created by $0 on $DATE
#
# start a container from image $NAME
#
# Usage: start [CMD]
# default CMD is $CMD
$cmd \$@
EOF
chmod a+x $WORK_DIR/start
# display final message
echo "--------------------------------------------"
echo "The image $NAME has been prepared in $WORK_DIR"
echo " Desktop launchers are available in $WORK_DIR/launchers"
echo "To start $NAME, use:"
echo " cd $WORK_DIR; ./start [cmd]"
echo " "

1096
scripts/debbisect Executable file

File diff suppressed because it is too large Load diff

131
scripts/debc.1 Normal file
View file

@ -0,0 +1,131 @@
.TH DEBC 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debc \- view contents of a generated Debian package
.SH SYNOPSIS
\fBdebc\fP [\fIoptions\fR] [\fIchanges file\fR] [\fIpackage\fR ...]
.SH DESCRIPTION
\fBdebc\fR figures out the current version of a package and displays
information about the \fI.deb\fR and \fI.udeb\fR files which have been generated
in the current build process. If a \fI.changes\fR file is specified
on the command line, the filename must end with \fI.changes\fR, as
this is how the program distinguishes it from package names. If not,
then \fBdebc\fR has to be called from within the source code directory
tree. In this case, it will look for the \fI.changes\fR file
corresponding to the current package version (by determining the name
and version number from the changelog, and the architecture in the
same way as \fBdpkg-buildpackage\fR(1) does). It then runs
\fBdpkg-deb \-I\fR and \fBdpkg-deb \-c\fR on every \fI.deb\fR and
\fI.udeb\fR archive listed in the \fI.changes\fR file to display
information about the contents of the \fI.deb\fR / \fI.udeb\fR files.
It precedes every \fI.deb\fR or \fI.udeb\fR file with the name of the
file. It assumes that all of the \fI.deb\fR / \fI.udeb\fR archives
live in the same directory as the \fI.changes\fR file. It is
useful for ensuring that the expected files have ended up in the
Debian package.
.PP
If a list of packages is given on the command line, then only those
debs or udebs with names in this list of packages will be processed.
.SH "Directory name checking"
In common with several other scripts in the \fBdevscripts\fR package,
\fBdebc\fR will climb the directory tree until it finds a
\fIdebian/changelog\fR file. As a safeguard against stray files
causing potential problems, it will examine the name of the parent
directory once it finds the \fIdebian/changelog\fR file, and check
that the directory name corresponds to the package name. Precisely
how it does this is controlled by two configuration file variables
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR and \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR, and
their corresponding command-line options \fB\-\-check-dirname-level\fR
and \fB\-\-check-dirname-regex\fR.
.PP
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR can take the following values:
.TP
.B 0
Never check the directory name.
.TP
.B 1
Only check the directory name if we have had to change directory in
our search for \fIdebian/changelog\fR. This is the default behaviour.
.TP
.B 2
Always check the directory name.
.PP
The directory name is checked by testing whether the current directory
name (as determined by \fBpwd\fR(1)) matches the regex given by the
configuration file option \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR or by the
command line option \fB\-\-check-dirname-regex\fR \fIregex\fR. Here
\fIregex\fR is a Perl regex (see \fBperlre\fR(3perl)), which will be
anchored at the beginning and the end. If \fIregex\fR contains a '/',
then it must match the full directory path. If not, then it must
match the full directory name. If \fIregex\fR contains the string
\'PACKAGE', this will be replaced by the source package name, as
determined from the changelog. The default value for the regex is:
\'PACKAGE(-.+)?', thus matching directory names such as PACKAGE and
PACKAGE-version.
.SH OPTIONS
.TP
\fB\-a\fIdebian-architecture\fR, \fB\-t\fIGNU-system-type\fR
See \fBdpkg-architecture\fR(1) for a description of these options.
They affect the search for the \fI.changes\fR file. They are provided
to mimic the behaviour of \fBdpkg-buildpackage\fR when determining the
name of the \fI.changes\fR file.
.TP
\fB\-\-debs\-dir\fR \fIdirectory\fR
Look for the \fI.changes\fR, \fI.deb\fR and \fI.udeb\fR files in
\fIdirectory\fR instead of the parent of the source directory.
This should either be an absolute path or relative to the top of the
source directory.
.TP
\fB\-\-check-dirname-level\fR \fIN\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-check-dirname-regex\fR \fIregex\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-list-changes\fR
List the filename of the .changes file, and do not display anything else. This
option only makes sense if a .changes file is NOT passed explicitly in the
command line. This can be used for example in a script that needs to reference
the .changes file, without having to duplicate the heuristics for finding it
that debc already implements.
.TP
\fB\-\-list-debs\fR
List the filenames of the .deb packages, and do not display their contents.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
\fB\-\-help\fR, \fB\-\-version\fR
Show help message and version information respectively.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variables are:
.TP
.B DEBRELEASE_DEBS_DIR
This specifies the directory in which to look for the \fI.changes\fR,
\fI.deb\fR and \fI.udeb\fR files, and is either an absolute path or
relative to the top of the source tree. This corresponds to the
\fB\-\-debs\-dir\fR command line option. This directive could be
used, for example, if you always use \fBpbuilder\fR or
\fBsvn-buildpackage\fR to build your packages. Note that it also
affects \fBdebrelease\fR(1) in the same way, hence the strange name of
the option.
.TP
.BR DEVSCRIPTS_CHECK_DIRNAME_LEVEL ", " DEVSCRIPTS_CHECK_DIRNAME_REGEX
See the above section \fBDirectory name checking\fR for an explanation of
these variables. Note that these are package-wide configuration
variables, and will therefore affect all \fBdevscripts\fR scripts
which check their value, as described in their respective manpages and
in \fBdevscripts.conf\fR(5).
.SH "SEE ALSO"
.BR debdiff (1),
.BR dpkg-deb (1),
.BR devscripts.conf (5)
.SH AUTHOR
Julian Gilbey <jdg@debian.org>, based on an original script by
Christoph Lameter <clameter@debian.org>.

1
scripts/debc.pl Symbolic link
View file

@ -0,0 +1 @@
debi.pl

496
scripts/debchange.1 Normal file
View file

@ -0,0 +1,496 @@
.TH DEBCHANGE 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debchange \- Tool for maintenance of the debian/changelog file in a source package
.SH SYNOPSIS
\fBdebchange\fR [\fIoptions\fR] [\fItext\fR ...]
.br
\fBdch\fR [\fIoptions\fR] [\fItext\fR ...]
.SH DESCRIPTION
\fBdebchange\fR or its alias \fBdch\fR will add a new comment line to
the Debian changelog in the current source tree. This command must be
run from within that tree. If the text of the change is given on the
command line, \fBdebchange\fR will run in batch mode and simply add the
text, with line breaks as necessary, at the appropriate place in
\fIdebian/changelog\fR (or the changelog specified by options, as described
below). If the text given on the command line is a null string,
\fBdebchange\fR will run in batch mode without adding any text. If the
text given on the command line is a space string, \fBdebchange\fR will run
in batch mode and add a blank changelog entry.
If no text is specified then \fBdebchange\fR
will run the editor as determined by \fBsensible-editor\fR for you to
edit the file. (The environment variables \fBVISUAL\fR and
\fBEDITOR\fR are used in this order to determine which editor to use.)
Editors which understand the \fI+n\fR option for starting the editing
on a specified line will use this to move to the correct line of the
file for editing. If the editor is quit without modifying the
temporary file, \fBdebchange\fR will exit without touching the
existing changelog. \fBNote that the changelog is assumed to be
encoded with the UTF-8 encoding. If it is not, problems may occur.\fR
Please see the \fBiconv\fR(1) manpage to find out how to convert
changelogs from legacy encodings. Finally, a \fIchangelog\fR or \fINEWS\fR
file can be created from scratch using the \fB\-\-create\fR option
described below.
.PP
\fBdebchange\fR also supports automatically producing bug-closing
changelog entries, using the \fB\-\-closes\fR option. This will
usually query the BTS, the Debian Bug Tracking System (see
https://bugs.debian.org/) to determine the title of the bug and the
package in which it occurs. This behaviour can be stopped by giving a
\fB\-\-noquery\fR option or by setting the configuration variable
\fBDEBCHANGE_QUERY_BTS\fR to \fIno\fR, as described below. In either
case, the editor (as described above) will always be invoked to give
an opportunity to modify the entries, and the changelog will be
accepted whether or not modifications are made. An extra changelog
entry can be given on the command line in addition to the closes
entries.
.PP
At most one of \fB\-\-append\fR, \fB\-\-increment\fR, \fB\-\-edit\fR,
\fB\-\-release\fR, and \fB\-\-newversion\fR may be specified as listed
below. If no options are specified, \fBdebchange\fR will use heuristics to
guess whether or not the package has been successfully released, and behave
as if \fB\-\-increment\fR had been specified if the package has been
released, or otherwise as if \fB\-\-append\fR has been specified.
.PP
Two different sets of heuristics can be used, as controlled by the
\fB\-\-release-heuristic\fR option or the
\fBDEBCHANGE_RELEASE_HEURISTIC\fR configuration variable. The default
\fIchangelog\fR heuristic assumes the package has been released unless its
changelog contains \fBUNRELEASED\fR in the distribution field. If this heuristic
is enabled then the distribution will default to \fBUNRELEASED\fR in new
changelog entries, and the \fB\-\-mainttrailer\fR option described below will be
automatically enabled. This can be useful if a package can be released by
different maintainers, or if you do not keep the upload logs. The alternate
\fIlog\fR heuristic determines if a package has been released by looking for an
appropriate \fBdupload\fR(1) or \fBdput\fR(1) log file in the parent directory.
A warning will be issued if the log file is found but a successful upload is not
recorded. This may be because the previous upload was performed with a version
of \fBdupload\fR prior to 2.1 or because the upload failed.
.PP
If either \fB\-\-increment\fR or \fB\-\-newversion\fR is used, the
name and email for the new version will be determined as follows. If
the environment variable \fBDEBFULLNAME\fR is set, this will be used
for the maintainer full name; if not, then \fBNAME\fR will be checked.
If the environment variable \fBDEBEMAIL\fR is set, this will be used
for the email address. If this variable has the form "name <email>",
then the maintainer name will also be taken from here if neither
\fBDEBFULLNAME\fR nor \fBNAME\fR is set. If this variable is not set,
the same test is performed on the environment variable \fBEMAIL\fR.
Next, if the full name has still not been determined, then use
\fBgetpwuid\fR(3) to determine the name from the password file. If
this fails, use the previous changelog entry. For the email address,
if it has not been set from \fBDEBEMAIL\fR or \fBEMAIL\fR, then look
in \fI/etc/mailname\fR, then attempt to build it from the username and
FQDN, otherwise use the email address in the previous changelog entry.
In other words, it's a good idea to set \fBDEBEMAIL\fR and
\fBDEBFULLNAME\fR when using this script.
.PP
Support is included for changelogs that record changes by multiple
co-maintainers of a package. If an entry is appended to the current
version's entries, and the maintainer is different from the maintainer who
is listed as having done the previous entries, then lines will be added to
the changelog to tell which maintainers made which changes. Currently only
one of the several such styles of recording this information is supported,
in which the name of the maintainer who made a set of changes appears
on a line before the changes, inside square brackets. This can be
switched on and off using the \fB\-\-\fR[\fBno\fR]\fBmultimaint\fR option or the
\fBDEBCHANGE_MULTIMAINT\fR configuration file option; the default is to
enable it. Note that if an entry has already been marked in this way,
then this option will be silently ignored.
.PP
If the directory name of the source tree has the form
\fIpackage\fR-\fIversion\fR, then \fBdebchange\fR will also attempt to
rename it if the (upstream) version number changes. This can be
prevented by using the \fB\-\-preserve\fR command line or
configuration file option as described below.
.PP
If \fB\-\-force\-bad\-version\fR or \fB\-\-allow\-lower\-version\fR is used,
\fBdebchange\fR will not stop if the new version is less than the current one.
This is especially useful while doing backports.
.SH "Directory name checking"
In common with several other scripts in the \fBdevscripts\fR package,
\fBdebchange\fR will climb the directory tree until it finds a
\fIdebian/changelog\fR file. As a safeguard against stray files
causing potential problems, it will examine the name of the parent
directory once it finds the \fIdebian/changelog\fR file, and check
that the directory name corresponds to the package name. Precisely
how it does this is controlled by two configuration file variables
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR and \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR, and
their corresponding command-line options \fB\-\-check-dirname-level\fR
and \fB\-\-check-dirname-regex\fR.
.PP
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR can take the following values:
.TP
.B 0
Never check the directory name.
.TP
.B 1
Only check the directory name if we have had to change directory in
our search for \fIdebian/changelog\fR. This is the default behaviour.
.TP
.B 2
Always check the directory name.
.PP
The directory name is checked by testing whether the current directory
name (as determined by \fBpwd\fR(1)) matches the regex given by the
configuration file option \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR or by the
command line option \fB\-\-check-dirname-regex\fR \fIregex\fR. Here
\fIregex\fR is a Perl regex (see \fBperlre\fR(3perl)), which will be
anchored at the beginning and the end. If \fIregex\fR contains a '\fB/\fR',
then it must match the full directory path. If not, then it must
match the full directory name. If \fIregex\fR contains the string
\'\fBPACKAGE\fR', this will be replaced by the source package name, as
determined from the changelog. The default value for the regex is:
\'\fBPACKAGE(-.+)?\fR', thus matching directory names such as \fBPACKAGE\fR and
\fBPACKAGE-\fIversion\fR.
.PP
The default changelog to be edited is \fIdebian/changelog\fR; however,
this can be changed using the \fB\-\-changelog\fR or \fB\-\-news\fR
options or the \fBCHANGELOG\fR environment variable, as described below.
.SH OPTIONS
.TP
.BR \-\-append ", " \-a
Add a new changelog entry at the end of the current version's entries.
.TP
.BR \-\-increment ", " \-i
Increment either the final component of the Debian release number or,
if this is a native Debian package, the version number. On Ubuntu or Tanglu,
this will also change the suffix from buildX to ubuntu1/tanglu1. Use
\fB\-R\fR, \fB\-\-rebuild\fR for a no change rebuild increment. This creates
a new section at the beginning of the changelog with appropriate
headers and footers. Also, if this is a new version of a native
Debian package, the directory name is changed to reflect this.
If \fBDEBCHANGE_RELEASE_HEURISTIC\fR is \fIchangelog\fR (default) and the
current release is \fIUNRELEASED\fR, this will only change the version of the
current changelog stanza. Otherwise, this will create a new changelog stanza
with the new version.
.TP
\fB\-\-newversion \fIversion\fR, \fB\-v \fIversion\fR
This specifies the version number (including the Debian release part)
explicitly and behaves as the \fB\-\-increment\fR option in other
respects. It will also change the directory name if the upstream
version number has changed.
If \fBDEBCHANGE_RELEASE_HEURISTIC\fR is \fIchangelog\fR (default) and the
current release is \fIUNRELEASED\fR, this will only change the version of the
current changelog stanza. Otherwise, this will create a new changelog stanza
with the new version.
.TP
.BR \-\-edit ", " \-e
Edit the changelog in an editor.
.TP
.BR \-\-release ", " \-r
Finalize the changelog for a release.
Update the changelog timestamp. If the distribution is set to
\fBUNRELEASED\fR, change it to the distribution from the previous changelog entry
(or another distribution as specified by \fB\-\-distribution\fR). If there are
no previous changelog entries and an explicit distribution has not been
specified, \fBunstable\fR will be used (or the name of the current development
release when run under Ubuntu).
.TP
.BR \-\-force\-save\-on\-release
When \fB\-\-release\fR is used, an editor is opened to allow inspection
of the changelog. The user is required to save the file to accept the modified
changelog, otherwise the original will be kept (default).
.TP
.BR \-\-no\-force\-save\-on\-release
Do not do so. Note that a dummy changelog entry may be supplied
in order to achieve the same effect - e.g. \fBdebchange \-\-release ""\fR.
The entry will not be added to the changelog but its presence will suppress
the editor.
.TP
.BR \-\-create
This will create a new \fIdebian/changelog\fR file (or \fINEWS\fR if
the \fB\-\-news\fR option is used). You must be in the top-level
directory to use this; no directory name checking will be performed.
The package name and version can either be specified using the
\fB\-\-package\fR and \fB\-\-newversion\fR options, determined from
the directory name using the \fB\-\-fromdirname\fR option or entered
manually into the generated \fIchangelog\fR file. The maintainer name is
determined from the environment if this is possible, and the
distribution is specified either using the \fB\-\-distribution\fR
option or in the generated \fIchangelog\fR file.
.TP
.BR \-\-empty
When used in combination with \fB\-\-create\fR, suppress the automatic
addition of an "\fBinitial release\fR" changelog entry (so that the next
invocation of \fBdebchange\fR adds the first entry). Note that this
will cause a \fBdpkg\-parsechangelog\fR warning on the next invocation
due to the lack of changes.
.TP
\fB\-\-package\fR \fIpackage\fR
This specifies the package name to be used in the new changelog; this
may only be used in conjunction with the \fB\-\-create\fR, \fB\-\-increment\fR and
\fB\-\-newversion\fR options.
.TP
.BR \-\-nmu ", " \-n
Increment the Debian release number for a non-maintainer upload by
either appending a "\fB.1\fR" to a non-NMU version number (unless the package
is Debian native, in which case "\fB+nmu1\fR" is appended) or by incrementing
an NMU version number, and add an NMU changelog comment. This happens
automatically if the packager is neither in the \fBMaintainer\fR nor the \fBUploaders\fR
field in \fIdebian/control\fR, unless \fBDEBCHANGE_AUTO_NMU\fR is set to
\fIno\fR or the \fB\-\-no\-auto\-nmu\fR option is used.
.TP
.BR \-\-bin\-nmu
Increment the Debian release number for a binary non-maintainer upload
by either appending a "\fB+b1\fR" to a non-binNMU version number or by
incrementing a binNMU version number, and add a binNMU changelog comment.
.TP
.BR \-\-qa ", " \-q
Increment the Debian release number for a Debian QA Team upload, and
add a \fBQA upload\fR changelog comment.
.TP
.BR \-\-rebuild ", " \-R
Increment the Debian release number for a no-change rebuild by
appending a "build1" or by incrementing a rebuild version number.
.TP
.BR \-\-security ", " \-s
Increment the Debian release number for a Debian Security Team non-maintainer
upload, and add a \fBSecurity Team upload\fR changelog comment.
.TP
.BR \-\-lts
Increment the Debian release number for a LTS Security Team non-maintainer
upload, and add a \fBLTS Security Team upload\fR changelog comment.
.TP
.B \-\-team
Increment the Debian release number for a team upload, and add a \fBTeam upload\fR
changelog comment.
.TP
.BR \-\-upstream ", " \-U
Don't append \fBdistro-name1\fR to the version on a derived
distribution. Increment the Debian version.
.TP
.B \-\-bpo
Increment the Debian release number for an upload to bullseye-backports,
and add a backport upload changelog comment.
.TP
.B \-\-stable
Increment the Debian release number for an upload to the current stable
release.
.TP
.BR \-\-local ", " \-l \fIsuffix\fR
Add a suffix to the Debian version number for a local build.
.TP
.BR \-\-force\-bad\-version ", " \-b
Force a version number to be less than the current one (e.g., when
backporting).
.TP
.B \-\-allow\-lower\-version \fIpattern\fR
Allow a version number to be less than the current one if the new version
matches the specified pattern.
.TP
.BR \-\-force\-distribution
Force the provided distribution to be used, even if it doesn't match the list of known
distributions (e.g. for unofficial distributions).
.TP
.BR \-\-auto\-nmu
Attempt to automatically determine whether a change to the changelog
represents a Non Maintainer Upload. This is the default.
.TP
.BR \-\-no\-auto\-nmu
Disable automatic NMU detection. Equivalent to setting
\fBDEBCHANGE_AUTO_NMU\fR to \fIno\fR.
.TP
.BR \-\-fromdirname ", " \-d
This will take the upstream version number from the directory name,
which should be of the form \fIpackage\fB-\fIversion\fR. If the
upstream version number has increased from the most recent changelog
entry, then a new entry will be made with version number
\fIversion\fB-1\fR (or \fIversion\fR if the package is Debian native),
with the same epoch as the previous package version. If the upstream
version number is the same, this option will behave in the same way as
\fB\-i\fR.
.TP
.BI \-\-closes " nnnnn\fR[\fB,\fInnnnn \fR...]
Add changelog entries to close the specified bug numbers. Also invoke
the editor after adding these entries. Will generate warnings if the
BTS cannot be contacted (and \fB\-\-noquery\fR has not been
specified), or if there are problems with the bug report located.
.TP
.B \-\-\fR[\fBno\fR]\fBquery
Should we attempt to query the BTS when generating closes entries?
.TP
.BR \-\-preserve ", " \-p
Preserve the source tree directory name if the upstream version number
(or the version number of a Debian native package) changes. See also
the configuration variables section below.
.TP
\fB\-\-no\-preserve\fR, \fB\-\-nopreserve\fR
Do not preserve the source tree directory name (default).
.TP
\fB\-\-vendor \fIvendor\fR
Override the distributor ID over the default returned by dpkg-vendor.
This name is used for heuristics applied to new package versions and for
sanity checking of the target distribution.
.TP
\fB\-\-distribution \fIdist\fR, \fB\-D \fIdist\fR
Use the specified distribution in the changelog entry being edited,
instead of using the previous changelog entry's distribution for new
entries or the existing value for existing entries.
.TP
\fB\-\-urgency \fIurgency\fR, \fB\-u \fIurgency\fR
Use the specified urgency in the changelog entry being edited,
instead of using the default "\fBmedium\fR" for new entries or the existing
value for existing entries.
.TP
\fB\-\-changelog \fIfile\fR, \fB\-c \fIfile\fR
This will edit the changelog \fIfile\fR instead of the standard
\fIdebian/changelog\fR. This option overrides any \fBCHANGELOG\fR
environment variable setting. Also, no directory traversing or
checking will be performed when this option is used.
.TP
\fB\-\-news\fR [\fInewsfile\fR]
This will edit \fInewsfile\fR (by default, \fIdebian/NEWS\fR) instead
of the regular changelog. Directory searching will be performed.
The changelog will be examined in order to determine the current package
version.
.TP
\fB\-\-\fR[\fBno\fR]\fBmultimaint\fR
Should we indicate that parts of a changelog entry have been made by
different maintainers? Default is yes; see the discussion above and
also the \fBDEBCHANGE_MULTIMAINT\fR configuration file option below.
.TP
\fB\-\-\fR[\fBno\fR]\fBmultimaint\-merge\fR
Should all changes made by the same author be merged into the same
changelog section? Default is yes; see the discussion above and also the
\fBDEBCHANGE_MULTIMAINT_MERGE\fR configuration file option below.
.TP
.BR \-\-maintmaint ", " \-m
Do not modify the maintainer details previously listed in the changelog.
This is useful particularly for sponsors wanting to automatically add a
sponsorship message without disrupting the other changelog details.
Note that there may be some interesting interactions if
multi-maintainer mode is in use; you will probably wish to check the
changelog manually before uploading it in such cases.
.TP
.BR \-\-controlmaint ", " \-M
Use maintainer details from the \fIdebian/control\fR \fBMaintainer\fR field
rather than relevant environment variables (\fBDEBFULLNAME\fR, \fBDEBEMAIL\fR,
etc.). This option might be useful to restore details of the main maintainer
in the changelog trailer after a bogus edit (e.g. when \fB\-m\fR was intended
but forgot) or when releasing a package in the name of the main maintainer
(e.g. the team).
.TP
.BR \-\-\fR[\fBno\fR]\fBmainttrailer ", " \-t
If \fBmainttrailer\fR is set, it will avoid modifying the existing changelog
trailer line (i.e. the maintainer and date-stamp details), unless
used with options that require the trailer to be modified
(e.g. \fB\-\-create\fR, \fB\-\-release\fR, \fB\-i\fR, \fB\-\-qa\fR, etc.)
This option differs from \fB\-\-maintmaint\fR in that it will use
multi-maintainer mode if appropriate, with the exception of editing the
trailer. See also the \fBDEBCHANGE_MAINTTRAILER\fR configuration file option
below.
.TP
\fB\-\-check-dirname-level\fR \fIN\fR
See the above section "\fBDirectory name checking\fR" for an explanation of
this option.
.TP
\fB\-\-check-dirname-regex\fR \fIregex\fR
See the above section "\fBDirectory name checking\fR" for an explanation of
this option.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
\fB\-\-release\-heuristic\fR \fIlog\fR|\fIchangelog\fR
Controls how \fBdebchange\fR determines if a package has been released,
when deciding whether to create a new changelog entry or append to an
existing changelog entry.
.TP
\fB\-\-date\fR \fIdate\fR
Use the specified date in the changelog entry being edited.
The date must be in RFC 5322 format, i.e. as produced by \fIdate -R\fR.
.TP
.BR \-\-help ", " \-h
Display a help message and exit successfully.
.TP
.B \-\-version
Display version and copyright information and exit successfully.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variables are:
.TP
.B DEBCHANGE_PRESERVE
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-preserve\fR command line parameter being used.
.TP
.B DEBCHANGE_QUERY_BTS
If this is set to \fIno\fR, then it is the same as the
\fB\-\-noquery\fR command line parameter being used.
.TP
.BR DEVSCRIPTS_CHECK_DIRNAME_LEVEL ", " DEVSCRIPTS_CHECK_DIRNAME_REGEX
See the above section "\fBDirectory name checking\fR" for an explanation of
these variables. Note that these are package-wide configuration
variables, and will therefore affect all \fBdevscripts\fR scripts
which check their value, as described in their respective manpages and
in \fBdevscripts.conf\fR(5).
.TP
.BR DEBCHANGE_RELEASE_HEURISTIC
Controls how \fBdebchange\fR determines if a package has been released,
when deciding whether to create a new changelog entry or append to an
existing changelog entry. Can be either \fIlog\fR or \fIchangelog\fR.
.TP
.BR DEBCHANGE_MULTIMAINT
If set to \fIno\fR, \fBdebchange\fR will not introduce multiple-maintainer
distinctions when a different maintainer appends an entry to an
existing changelog. See the discussion above. Default is \fIyes\fR.
.TP
.BR DEBCHANGE_MULTIMAINT_MERGE
If set to \fIyes\fR, when adding changes in multiple-maintainer mode
\fBdebchange\fR will check whether previous changes by the current
maintainer exist and add the new changes to the existing block
rather than creating a new block. Default is \fIno\fR.
.TP
.BR DEBCHANGE_MAINTTRAILER
If this is set to \fIno\fR, then it is the same as the
\fB\-\-nomainttrailer\fR command line parameter being used.
.TP
.BR DEBCHANGE_TZ
Use this timezone for changelog entries. Default is the user/system
timezone as shown by `\fBdate \-R\fR` and affected by the environment variable \fBTZ\fR.
.TP
.BR DEBCHANGE_LOWER_VERSION_PATTERN
If this is set, then it is the same as the
\fB\-\-allow\-lower\-version\fR command line parameter being used.
.TP
.BR DEBCHANGE_AUTO_NMU
If this is set to \fIno\fR then \fBdebchange\fR will not attempt to
automatically determine whether the current changelog stanza represents
an NMU. The default is \fIyes\fR. See the discussion of the
\fB\-\-nmu\fR option above.
.TP
.BR DEBCHANGE_FORCE_SAVE_ON_RELEASE
If this is set to \fIno\fR, then it is the same as the
\fB\-\-no\-force\-save\-on\-release\fR command line parameter being used.
.TP
.B DEBCHANGE_VENDOR
Use this vendor instead of the default (dpkg-vendor output). See
\fB\-\-vendor\fR for details.
.SH ENVIRONMENT
.TP
.BR DEBEMAIL ", " EMAIL ", " DEBFULLNAME ", " NAME
See the above description of the use of these environment variables.
.TP
.B CHANGELOG
This variable specifies the changelog to edit in place of
\fIdebian/changelog\fR. No directory traversal or checking is
performed when this variable is set. This variable is overridden by
the \fB\-\-changelog\fR command-line setting.
.TP
.BR VISUAL ", " EDITOR
These environment variables (in this order) determine the editor used
by \fBsensible-editor\fR.
.SH "SEE ALSO"
.BR debc (1),
.BR debclean (1),
.BR dput (1),
.BR dupload (1),
.BR devscripts.conf (5),
.BR gbp-dch (1)
.SH AUTHOR
The original author was Christoph Lameter <clameter@debian.org>.
Many substantial changes and improvements were made by Julian Gilbey
<jdg@debian.org>.

View file

@ -0,0 +1,88 @@
# /usr/share/bash-completion/completions/debchange
# Bash command completion for debchange(1).
# Documentation: bash(1), section “Programmable Completion”.
_debchange()
{
local cur prev options
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD-1]}
options='-a --append -i --increment -v --newversion -e --edit\
-r --release --force-save-on-release --no-force-save-on-release\
--create --empty --package --auto-nmu --no-auto-nmu -n --nmu --lts\
--bin-nmu -q --qa -R --rebuild -s --security --team -U --upstream\
--bpo --stable -l --local -b --force-bad-version --allow-lower-version\
--force-distribution --closes --noquery --query -d --fromdirname\
-p --preserve --no-preserve --vendor -D --distribution\
-u --urgency -c --changelog --news --nomultimaint --multimaint\
--nomultimaint-merge --multimaint-merge -m --maintmaint\
-M --controlmaint -t --mainttrailer --check-dirname-level\
--check-dirname-regex --no-conf --noconf --release-heuristic\
--help -h --version'
#--------------------------------------------------------------------------
#FIXME: I don't want hard-coding codename...
#--------------------------------------------------------------------------
oldstable_codename='bullseye'
stable_codename='bookworm'
testing_codename='trixie'
distro="oldstable-security oldstable-proposed-updates\
"$oldstable_codename"-security\
"$oldstable_codename"-backports\
"$oldstable_codename"-backports-sloppy\
stable-security stable-proposed-updates\
"$stable_codename"-security\
"$stable_codename"-backports\
"$stable_codename"-updates\
testing-security testing-proposed-updates\
"$testing_codename"-security\
unstable experimental"
urgency='low medium high critical'
case $prev in
--changelog | -c | --news)
COMPREPLY=( $( compgen -G "${cur}*" ) )
;;
--check-dirname-level)
COMPREPLY=( $( compgen -W [0 1 2] ) )
;;
#FIXME: we need "querybts --list" option with no verbose output
# --closes)
# package=`dpkg-parsechangelog -SSource`
# bugnumber=`querybts --list -b $package|grep ^#|cut -d' ' -f1`
# COMPREPLY=( $( compgen -W "$bugnumber" ) )
# ;;
-D | --distribution)
COMPREPLY=( $( compgen -W "$distro" ) )
;;
--newversion | -v | --package | --local | -l | --allow-lower-version)
;;
--release-heuristic)
COMPREPLY=( $( compgen -W 'log changelog' ) )
;;
-u | --urgency)
COMPREPLY=( $( compgen -W "$urgency" ) )
;;
*)
COMPREPLY=( $(
compgen -W "$options" | grep "^$cur"
) )
;;
esac
return 0
}
complete -F _debchange debchange dch
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

1945
scripts/debchange.pl Executable file

File diff suppressed because it is too large Load diff

1243
scripts/debcheckout.pl Executable file

File diff suppressed because it is too large Load diff

115
scripts/debclean.1 Normal file
View file

@ -0,0 +1,115 @@
.TH DEBCLEAN 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debclean \- clean up a sourcecode tree
.SH SYNOPSIS
\fBdebclean\fR [\fIoptions\fR]
.SH DESCRIPTION
\fBdebclean\fR walks through the directory tree starting at the
directory tree in which it was invoked, and executes
.I debuild -- clean
for each Debian source directory encountered. These directories are
recognised by containing a debian/changelog file for a package whose
name matches that of the directory. Name matching is described below.
.PP
If \fBdebclean\fR is invoked from a directory that is already a Debian source
package, it will not descend into its subdirectories.
.PP
Also, if the \fB\-\-cleandebs\fR option is given, then in every
directory containing a Debian source tree, all files named *.deb,
*.changes and *.build are removed. The .dsc, .diff.gz and
the (.orig).tar.gz files are not touched so that the release can be
reconstructed if necessary, and the .upload files are left so that
\fBdebchange\fR functions correctly. The \fB\-\-nocleandebs\fR option
prevents this extra cleaning behaviour and the \fB\-\-cleandebs\fR
option forces it. The default is not to clean these files.
.PP
\fBdebclean\fR uses \fBdebuild\fR(1) to clean the source tree.
.SH "Directory name checking"
In common with several other scripts in the \fBdevscripts\fR package,
\fBdebclean\fR will walk through the directory tree searching for
\fIdebian/changelog\fR files. As a safeguard against stray files
causing potential problems, it will examine the name of the parent
directory once it finds a \fIdebian/changelog\fR file, and check
that the directory name corresponds to the package name. Precisely
how it does this is controlled by two configuration file variables
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR and \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR, and
their corresponding command-line options \fB\-\-check-dirname-level\fR
and \fB\-\-check-dirname-regex\fR.
.PP
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR can take the following values:
.TP
.B 0
Never check the directory name.
.TP
.B 1
Only check the directory name if we have had to change directory in
our search for \fIdebian/changelog\fR. This is the default behaviour.
.TP
.B 2
Always check the directory name.
.PP
The directory name is checked by testing whether the current directory
name (as determined by \fBpwd\fR(1)) matches the regex given by the
configuration file option \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR or by the
command line option \fB\-\-check-dirname-regex\fR \fIregex\fR. Here
\fIregex\fR is a Perl regex (see \fBperlre\fR(3perl)), which will be
anchored at the beginning and the end. If \fIregex\fR contains a '/',
then it must match the full directory path. If not, then it must
match the full directory name. If \fIregex\fR contains the string
\'PACKAGE', this will be replaced by the source package name, as
determined from the changelog. The default value for the regex is:
\'PACKAGE(-.+)?', thus matching directory names such as PACKAGE and
PACKAGE-version.
.SH OPTIONS
.TP
.B \-\-cleandebs
Also remove all .deb, .changes and .build files from the parent
directory.
.TP
.B \-\-nocleandebs
Do not remove the .deb, .changes and .build files from the parent
directory; this is the default behaviour.
.TP
\fB\-\-check-dirname-level\fR \fIN\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-check-dirname-regex\fR \fIregex\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
.B \-d
Do not run dpkg-checkbuilddeps to check build dependencies.
.TP
.B \-\-help
Display a help message and exit successfully.
.TP
.B \-\-version
Display version and copyright information and exit successfully.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variables are:
.TP
.B DEBCLEAN_CLEANDEBS
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-cleandebs\fR command line parameter being used.
.TP
.BR DEVSCRIPTS_CHECK_DIRNAME_LEVEL ", " DEVSCRIPTS_CHECK_DIRNAME_REGEX
See the above section \fBDirectory name checking\fR for an explanation of
these variables. Note that these are package-wide configuration
variables, and will therefore affect all \fBdevscripts\fR scripts
which check their value, as described in their respective manpages and
in \fBdevscripts.conf\fR(5).
.SH "SEE ALSO"
.BR debuild (1),
.BR devscripts.conf (5)
.SH AUTHOR
Christoph Lameter <clameter@debian.org>;
modifications by Julian Gilbey <jdg@debian.org>.

218
scripts/debclean.sh Executable file
View file

@ -0,0 +1,218 @@
#!/bin/bash
set -e
PROGNAME=${0##*/}
MODIFIED_CONF_MSG='Default settings modified by devscripts configuration files:'
usage() {
echo \
"Usage: $PROGNAME [options]
Clean all debian build trees under current directory.
Options:
--cleandebs Also remove all .deb, .changes and .build
files from the parent of each build tree
--nocleandebs Don't remove the .deb etc. files (default)
--check-dirname-level N
How much to check directory names before cleaning trees:
N=0 never
N=1 only if program changes directory (default)
N=2 always
--check-dirname-regex REGEX
What constitutes a matching directory name; REGEX is
a Perl regular expression; the string \`PACKAGE' will
be replaced by the package name; see manpage for details
(default: 'PACKAGE(-.+)?')
--no-conf, --noconf
Do not read devscripts config files;
must be the first option given
-d Do not run dpkg-checkbuilddeps to check build dependencies
--help Display this help message and exit
--version Display version information
$MODIFIED_CONF_MSG"
}
version() {
echo \
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
This code is copyright 1999 by Julian Gilbey, all rights reserved.
Original code by Christoph Lameter.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later."
}
# Boilerplate: set config variables
DEFAULT_DEBCLEAN_CLEANDEBS=no
DEFAULT_DEVSCRIPTS_CHECK_DIRNAME_LEVEL=1
DEFAULT_DEVSCRIPTS_CHECK_DIRNAME_REGEX='PACKAGE(-.+)?'
VARS="DEBCLEAN_CLEANDEBS DEVSCRIPTS_CHECK_DIRNAME_LEVEL DEVSCRIPTS_CHECK_DIRNAME_REGEX"
if [ "$1" = "--no-conf" -o "$1" = "--noconf" ]; then
shift
MODIFIED_CONF_MSG="$MODIFIED_CONF_MSG
(no configuration files read)"
# set defaults
for var in $VARS; do
eval "$var=\$DEFAULT_$var"
done
else
# Run in a subshell for protection against accidental errors
# in the config files
eval $(
set +e
for var in $VARS; do
eval "$var=\$DEFAULT_$var"
done
for file in /etc/devscripts.conf ~/.devscripts
do
[ -r $file ] && . $file
done
set | grep -E '^(DEBCLEAN|DEVSCRIPTS)_')
# check sanity
case "$DEBCLEAN_CLEANDEBS" in
yes|no) ;;
*) DEBCLEAN_CLEANDEBS=no ;;
esac
case "$DEVSCRIPTS_CHECK_DIRNAME_LEVEL" in
0|1|2) ;;
*) DEVSCRIPTS_CHECK_DIRNAME_LEVEL=1 ;;
esac
# set config message
MODIFIED_CONF=''
for var in $VARS; do
eval "if [ \"\$$var\" != \"\$DEFAULT_$var\" ]; then
MODIFIED_CONF_MSG=\"\$MODIFIED_CONF_MSG
$var=\$$var\";
MODIFIED_CONF=yes;
fi"
done
if [ -z "$MODIFIED_CONF" ]; then
MODIFIED_CONF_MSG="$MODIFIED_CONF_MSG
(none)"
fi
fi
# synonyms
CHECK_DIRNAME_LEVEL="$DEVSCRIPTS_CHECK_DIRNAME_LEVEL"
CHECK_DIRNAME_REGEX="$DEVSCRIPTS_CHECK_DIRNAME_REGEX"
# Need -o option to getopt or else it doesn't work
TEMP=$(getopt -s bash -o "" -o d \
--long cleandebs,nocleandebs,no-cleandebs \
--long no-conf,noconf \
--long check-dirname-level:,check-dirname-regex: \
--long help,version -n "$PROGNAME" -- "$@")
if [ $? != 0 ] ; then exit 1 ; fi
eval set -- $TEMP
# Process Parameters
while [ "$1" ]; do
case $1 in
--cleandebs) DEBCLEAN_CLEANDEBS=yes ;;
--nocleandebs|--no-cleandebs) DEBCLEAN_CLEANDEBS=no ;;
--check-dirname-level)
shift
case "$1" in
0|1|2) CHECK_DIRNAME_LEVEL=$1 ;;
*) echo "$PROGNAME: unrecognised --check-dirname-level value (allowed are 0,1,2)" >&2
exit 1 ;;
esac
;;
-d)
CHECKBUILDDEP="-d" ;;
--check-dirname-regex)
shift; CHECK_DIRNAME_REGEX="$1" ;;
--no-conf|--noconf)
echo "$PROGNAME: $1 is only acceptable as the first command-line option!" >&2
exit 1 ;;
--help) usage; exit 0 ;;
--version) version; exit 0 ;;
--) shift; break ;;
*) echo "$PROGNAME: bug in option parser, sorry!" >&2 ; exit 1 ;;
esac
shift
done
# Still going?
if [ $# -gt 0 ]; then
echo "$PROGNAME takes no non-option arguments;" >&2
echo "try $PROGNAME --help for usage information" >&2
exit 1
fi
# Script to clean up debian directories
OPWD="$(pwd)"
TESTDIR=$(echo $OPWD | grep -Eo '.*/debian/?' | sed 's/\/debian\/\?$//')
if [ -f debian/changelog ]; then
directories=$OPWD
elif [ -f "$TESTDIR/debian/changelog" ]; then
directories=$TESTDIR
else
directories=$(find . -type d -name "debian" -a ! -wholename '*.git*/debian')
fi
for i in $directories; do
( # subshell to not lose where we are
DIR=${i%/debian}
echo "Cleaning in directory $DIR"
cd $DIR
# Clean up the source package, but only if the directory looks like
# a genuine build tree
if [ ! -f debian/changelog ]; then
echo "Directory $DIR: contains no debian/changelog, skipping" >&2
exit
fi
package="$(dpkg-parsechangelog -SSource)"
if [ -z "$package" ]; then
echo "Directory $DIR: unable to determine package name, skipping" >&2
exit
fi
# let's test the directory name if appropriate
if [ $CHECK_DIRNAME_LEVEL -eq 2 -o \
\( $CHECK_DIRNAME_LEVEL -eq 1 -a "$OPWD" != "$(pwd)" \) ]; then
if ! perl -MFile::Basename -w \
-e "\$pkg='$package'; \$re='$CHECK_DIRNAME_REGEX';" \
-e '$re =~ s/PACKAGE/\\Q$pkg\\E/g; $pwd=`pwd`; chomp $pwd;' \
-e 'if ($re =~ m%/%) { eval "exit (\$pwd =~ /^$re\$/ ? 0:1);"; }' \
-e 'else { eval "exit (basename(\$pwd) =~ /^$re\$/ ? 0:1);"; }'
then
echo "Full directory path $(pwd) does not match package name, skipping." >&2
echo "Run $PROGNAME --help for more information on directory name matching." >&2
exit
fi
fi
# We now know we're OK and debuild won't complain about the dirname
debuild $CHECKBUILDDEP -- clean
# Clean up the package related files
if [ "$DEBCLEAN_CLEANDEBS" = yes ]; then
cd ..
rm -f *.changes *.deb *.build
fi
)
done

976
scripts/debcommit.pl Executable file
View file

@ -0,0 +1,976 @@
#!/usr/bin/perl
=head1 NAME
debcommit - commit changes to a package
=head1 SYNOPSIS
B<debcommit> [I<options>] [B<--all> | I<files to commit>]
=head1 DESCRIPTION
B<debcommit> generates a commit message based on new text in B<debian/changelog>,
and commits the change to a package's repository. It must be run in a working
copy for the package. Supported version control systems are:
B<cvs>, B<git>, B<hg> (mercurial), B<svk>, B<svn> (Subversion),
B<baz>, B<bzr>, B<tla> (arch), B<darcs>.
=head1 OPTIONS
=over 4
=item B<-c>, B<--changelog> I<path>
Specify an alternate location for the changelog. By default debian/changelog is
used.
=item B<-r>, B<--release>
Commit a release of the package. The version number is determined from
debian/changelog, and is used to tag the package in the repository.
Note that svn/svk tagging conventions vary, so debcommit uses
svnpath(1) to determine where the tag should be placed in the
repository.
=item B<-R>, B<--release-use-changelog>
When used in conjunction with B<--release>, if there are uncommitted
changes to the changelog then derive the commit message from those
changes rather than using the default message.
=item B<-m> I<text>, B<--message> I<text>
Specify a commit message to use. Useful if the program cannot determine
a commit message on its own based on debian/changelog, or if you want to
override the default message.
=item B<-n>, B<--noact>
Do not actually do anything, but do print the commands that would be run.
=item B<-d>, B<--diff>
Instead of committing, do print the diff of what would have been committed if
this option were not given. A typical usage scenario of this option is the
generation of patches against the current working copy (e.g. when you don't have
commit access right).
=item B<-C>, B<--confirm>
Display the generated commit message and ask for confirmation before committing
it. It is also possible to edit the message at this stage; in this case, the
confirmation prompt will be re-displayed after the editing has been performed.
=item B<-e>, B<--edit>
Edit the generated commit message in your favorite editor before committing
it.
=item B<-a>, B<--all>
Commit all files. This is the default operation when using a VCS other
than git.
=item B<-s>, B<--strip-message>, B<--no-strip-message>
If this option is set and the commit message has been derived from the
changelog, the characters "* " will be stripped from the beginning of
the message.
This option is set by default and ignored if more than one line of
the message begins with "[*+-] ".
=item B<--sign-commit>, B<--no-sign-commit>
If this option is set, then the commits that debcommit creates will be
OpenPGP signed. Currently this is only supported by git, hg, and bzr.
=item B<--sign-tags>, B<--no-sign-tags>
If this option is set, then tags that debcommit creates will be OpenPGP
signed. Currently this is only supported by git.
=item B<--signoff>, B<--no-signoff>
If this option is set, add a "Signed-off-by:" line to the commit message.
=item B<--changelog-info>
If this option is set, the commit author and date will be determined from
the Maintainer and Date field of the first paragraph in F<debian/changelog>.
This is mainly useful when using B<debchange>(1) with the B<--no-mainttrailer>
option.
=back
=head1 CONFIGURATION VARIABLES
The two configuration files F</etc/devscripts.conf> and
F<~/.devscripts> are sourced by a shell in that order to set
configuration variables. Command line options can be used to override
configuration file settings. Environment variable settings are
ignored for this purpose. The currently recognised variables are:
=over 4
=item B<DEBCOMMIT_STRIP_MESSAGE>
If this is set to I<no>, then it is the same as the B<--no-strip-message>
command line parameter being used. The default is I<yes>.
=item B<DEBCOMMIT_SIGN_TAGS>
If this is set to I<yes>, then it is the same as the B<--sign-tags> command
line parameter being used. The default is I<no>.
=item B<DEBCOMMIT_SIGN_COMMITS>
If this is set to I<yes>, then it is the same as the B<--sign-commit>
command line parameter being used. The default is I<no>.
=item B<DEBCOMMIT_SIGNOFF>
If this is set to I<yes>, then it is the same as the B<--signoff> command
line parameter being used. The default is I<no>.
=item B<DEBCOMMIT_RELEASE_USE_CHANGELOG>
If this is set to I<yes>, then it is the same as the B<--release-use-changelog>
command line parameter being used. The default is I<no>.
=item B<DEBSIGN_KEYID>
This is the key id used for signing tags. If not set, a default will be
chosen by the revision control system.
=back
=head1 VCS SPECIFIC FEATURES
=over 4
=item B<tla> / B<baz>
If the commit message contains more than 72 characters, a summary will
be created containing as many full words from the message as will fit within
72 characters, followed by an ellipsis.
=back
Each of the features described below is applicable only if the commit message
has been automatically determined from the changelog.
=over 4
=item B<git>
If only a single change is detected in the changelog, B<debcommit> will unfold
it to a single line and behave as if B<--strip-message> was used.
Otherwise, the first change will be unfolded and stripped to form a summary line
and a commit message formed using the summary line followed by a blank line and
the changes as extracted from the changelog. B<debcommit> will then spawn an
editor so that the message may be fine-tuned before committing.
=item B<hg> / B<darcs>
The first change detected in the changelog will be unfolded to form a single line
summary. If multiple changes were detected then an editor will be spawned to
allow the message to be fine-tuned.
=item B<bzr>
If the changelog entry used for the commit message closes any bugs then B<--fixes>
options to "bzr commit" will be generated to associate the revision and the bugs.
=back
=cut
use warnings;
use strict;
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use Cwd;
use File::Basename;
use File::HomeDir;
use File::Temp;
my $progname = basename($0);
my $modified_conf_msg;
sub usage {
print <<"EOT";
Usage: $progname [options] [files to commit]
$progname --version
$progname --help
Generates a commit message based on new text in debian/changelog,
and commit the change to a package\'s repository.
Options:
-c --changelog=path Specify the location of the changelog
-r --release Commit a release of the package and create a tag
-R --release-use-changelog
Take any uncommitted changes in the changelog in
to account when determining the commit message
for a release
-m --message=text Specify a commit message
-n --noact Dry run, no actual commits
-d --diff Print diff on standard output instead of committing
-C --confirm Ask for confirmation of the message before commit
-e --edit Edit the message in EDITOR before commit
-a --all Commit all files (default except for git)
-s --strip-message Strip the leading '* ' from the commit message (default)
--no-strip-message Do not strip a leading '* '
--sign-commit Enable signing of the commit (git, hg, and bzr)
--no-sign-commit Do not sign the commit (default)
--sign-tags Enable signing of tags (git only)
--no-sign-tags Do not sign tags (default)
--signoff Add a Signed-off-by line to the commit message
--no-signoff Do not add a Signed-off-by line to the commit message (default)
--changelog-info Use author and date information from the changelog
for the commit (git, hg, and bzr)
-h --help This message
-v --version Version information
--no-conf, --noconf
Don\'t read devscripts config files;
must be the first option given
Default settings modified by devscripts configuration files:
$modified_conf_msg
EOT
}
sub version {
print <<"EOF";
This is $progname, from the Debian devscripts package, version ###VERSION###
This code is copyright by Joey Hess <joeyh\@debian.org>, all rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
EOF
}
my $release = 0;
my $message;
my $release_use_changelog = 0;
my $noact = 0;
my $diffmode = 0;
my $confirm = 0;
my $edit = 0;
my $all = 0;
my $stripmessage = 1;
my $signcommit = 0;
my $signtags = 0;
my $signoff = 0;
my $changelog;
my $changelog_info = 0;
my $keyid;
my ($package, $version, $date, $maintainer);
my $onlydebian = 0;
# Now start by reading configuration files and then command line
# The next stuff is boilerplate
if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
$modified_conf_msg = " (no configuration files read)";
shift;
} else {
my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
my %config_vars = (
'DEBCOMMIT_STRIP_MESSAGE' => 'yes',
'DEBCOMMIT_SIGN_COMMITS' => 'no',
'DEBCOMMIT_SIGN_TAGS' => 'no',
'DEBCOMMIT_SIGNOFF' => 'no',
'DEBCOMMIT_RELEASE_USE_CHANGELOG' => 'no',
'DEBSIGN_KEYID' => '',
);
my %config_default = %config_vars;
my $shell_cmd;
# Set defaults
foreach my $var (keys %config_vars) {
$shell_cmd .= qq[$var="$config_vars{$var}";\n];
}
$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
# Read back values
foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
my $shell_out = `/bin/bash -c '$shell_cmd'`;
@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
# Check validity
$config_vars{'DEBCOMMIT_STRIP_MESSAGE'} =~ /^(yes|no)$/
or $config_vars{'DEBCOMMIT_STRIP_MESSAGE'} = 'yes';
$config_vars{'DEBCOMMIT_SIGN_COMMITS'} =~ /^(yes|no)$/
or $config_vars{'DEBCOMMIT_SIGN_COMMITS'} = 'no';
$config_vars{'DEBCOMMIT_SIGN_TAGS'} =~ /^(yes|no)$/
or $config_vars{'DEBCOMMIT_SIGN_TAGS'} = 'no';
$config_vars{'DEBCOMMIT_RELEASE_USE_CHANGELOG'} =~ /^(yes|no)$/
or $config_vars{'DEBCOMMIT_RELEASE_USE_CHANGELOG'} = 'no';
foreach my $var (sort keys %config_vars) {
if ($config_vars{$var} ne $config_default{$var}) {
$modified_conf_msg .= " $var=$config_vars{$var}\n";
}
}
$modified_conf_msg ||= " (none)\n";
chomp $modified_conf_msg;
$stripmessage = $config_vars{'DEBCOMMIT_STRIP_MESSAGE'} eq 'no' ? 0 : 1;
$signcommit = $config_vars{'DEBCOMMIT_SIGN_COMMITS'} eq 'no' ? 0 : 1;
$signtags = $config_vars{'DEBCOMMIT_SIGN_TAGS'} eq 'no' ? 0 : 1;
$signoff = $config_vars{'DEBCOMMIT_SIGNOFF'} eq 'no' ? 0 : 1;
$release_use_changelog
= $config_vars{'DEBCOMMIT_RELEASE_USE_CHANGELOG'} eq 'no' ? 0 : 1;
if (exists $config_vars{'DEBSIGN_KEYID'}
&& length $config_vars{'DEBSIGN_KEYID'}) {
$keyid = $config_vars{'DEBSIGN_KEYID'};
}
}
# Find a good default for the changelog file location
for (qw"debian/changelog changelog") {
if (-e $_) {
$changelog = $_;
last;
}
}
# Now read the command line arguments
if (
!GetOptions(
"r|release" => \$release,
"m|message=s" => \$message,
"n|noact" => \$noact,
"d|diff" => \$diffmode,
"C|confirm" => \$confirm,
"e|edit" => \$edit,
"a|all" => \$all,
"c|changelog=s" => \$changelog,
"s|strip-message!" => \$stripmessage,
"sign-commit!" => \$signcommit,
"sign-tags!" => \$signtags,
"signoff!" => \$signoff,
"changelog-info!" => \$changelog_info,
"R|release-use-changelog!" => \$release_use_changelog,
"h|help" => sub { usage(); exit 0; },
"v|version" => sub { version(); exit 0; },
'noconf|no-conf' => sub { die '--noconf must be first option'; },
)
) {
die "Usage: $progname [options] [--all | files to commit]\n";
}
if ($diffmode) {
$confirm = 0;
$edit = 0;
}
my @files_to_commit = @ARGV;
if (@files_to_commit && !grep(/$changelog/, @files_to_commit)) {
push @files_to_commit, $changelog;
}
# Main program
my $prog = getprog();
if (!defined $changelog) {
die "debcommit: Could not find a Debian changelog\n";
}
if (!-e $changelog) {
die "debcommit: cannot find $changelog\n";
}
$message = getmessage()
if !defined $message and (not $release or $release_use_changelog);
if ($release || $changelog_info) {
require Dpkg::Changelog::Parse;
my $log = Dpkg::Changelog::Parse::changelog_parse(file => $changelog);
if ($release) {
if ($log->{Distribution} =~ /UNRELEASED/) {
die
"debcommit: $changelog says it's UNRELEASED\nTry running dch --release first\n";
}
$package = $log->{Source};
$version = $log->{Version};
$message = "releasing package $package version $version"
if !defined $message;
}
if ($changelog_info) {
$maintainer = $log->{Maintainer};
$date = $log->{Date};
}
}
if ($edit) {
my $modified = 0;
($message, $modified) = edit($message);
die "$progname: Commit message not modified / saved; aborting\n"
unless $modified;
}
if (not $confirm or confirm($message)) {
commit($message);
tag($package, $version) if $release;
}
# End of code, only subs below
sub getprog {
if (-d "debian") {
if (-d "debian/.svn") {
# SVN has .svn even in subdirs...
if (!-d ".svn") {
$onlydebian = 1;
}
return "svn";
} elsif (-d "debian/CVS") {
# CVS has CVS even in subdirs...
if (!-d "CVS") {
$onlydebian = 1;
}
return "cvs";
} elsif (-d "debian/{arch}") {
# I don't think we can tell just from the working copy
# whether to use tla or baz, so try baz if it's available,
# otherwise fall back to tla.
if (system("baz --version >/dev/null 2>&1") == 0) {
return "baz";
} else {
return "tla";
}
} elsif (-d "debian/_darcs") {
$onlydebian = 1;
return "darcs";
}
}
if (-e ".git") {
# With certain forms of git checkouts, .git can be a file instead of a directory
return "git";
}
if (-d ".svn") {
return "svn";
}
if (-d "CVS") {
return "cvs";
}
if (-d "{arch}") {
# I don't think we can tell just from the working copy
# whether to use tla or baz, so try baz if it's available,
# otherwise fall back to tla.
if (system("baz --version >/dev/null 2>&1") == 0) {
return "baz";
} else {
return "tla";
}
}
if (-d ".bzr") {
return "bzr";
}
if (-d ".hg") {
return "hg";
}
if (-d "_darcs") {
return "darcs";
}
# Test for this file to avoid interactive prompting from svk.
if (-d File::HomeDir->my_home . "/.svk/local") {
# svk has no useful directories so try to run it.
my $svkpath
= `svk info . 2>/dev/null| grep -i '^Depot Path:' | cut -d ' ' -f 3`;
if (length $svkpath) {
return "svk";
}
}
# .bzr, .git, .hg, or .svn may be in a parent directory, rather than the
# current directory, if multiple packages are kept in one repository.
my $dir = getcwd();
while ($dir =~ s/[^\/]*\/?$// && length $dir) {
if (-d "$dir/.bzr") {
return "bzr";
}
if (-e "$dir/.git") {
return "git";
}
if (-d "$dir/.hg") {
return "hg";
}
if (-d "$dir/.svn") {
return "svn";
}
}
die
"debcommit: not in a cvs, Subversion, baz, bzr, git, hg, svk or darcs working copy\n";
}
sub action {
my $prog = shift;
if ($prog eq "darcs" && $onlydebian) {
splice(@_, 1, 0, "--repodir=debian");
}
print $prog, " ", join(
" ",
map {
if (/[^-A-Za-z0-9]/) { "'$_'" }
else { $_ }
} @_
),
"\n";
return 1 if $noact;
return (system($prog, @_) != 0) ? 0 : 1;
}
sub bzr_find_fixes {
my $message = shift;
require Dpkg::Changelog::Entry::Debian;
require Dpkg::Vendor::Ubuntu;
my @debian_closes = Dpkg::Changelog::Entry::Debian::find_closes($message);
my $launchpad_closes
= Dpkg::Vendor::Ubuntu::find_launchpad_closes($message);
my @fixes_arg = ();
map { push(@fixes_arg, ("--fixes", "deb:" . $_)) } @debian_closes;
map { push(@fixes_arg, ("--fixes", "lp:" . $_)) } @$launchpad_closes;
return @fixes_arg;
}
sub commit {
my $message = shift;
die "debcommit: can't specify a list of files to commit when using --all\n"
if (@files_to_commit and $all);
my $action_rc; # return code of external command
if ($prog =~ /^(cvs|svn|svk|hg)$/) {
if (!@files_to_commit && $onlydebian) {
@files_to_commit = ("debian");
}
my @extra_args;
if ($changelog_info && $prog eq 'hg') {
push(@extra_args, '-u', $maintainer, '-d', $date);
}
$action_rc
= $diffmode
? action($prog, "diff", @files_to_commit)
: action($prog, "commit", "-m", $message, @extra_args,
@files_to_commit);
if ($prog eq 'hg' && $action_rc && $signcommit) {
my @sign_args;
push(@sign_args, '-k', $keyid) if $keyid;
push(@sign_args, '-u', $maintainer, '-d', $date)
if $changelog_info;
if (!action($prog, 'sign', @sign_args)) {
die "$progname: failed to sign commit\n";
}
}
} elsif ($prog eq 'git') {
if (!@files_to_commit && ($all || $release)) {
# check to see if the WC is clean. git-commit would exit
# nonzero, so don't run it in --all or --release mode.
my $status = `git status --porcelain`;
if (!$status) {
print $status;
return;
}
}
if ($diffmode) {
$action_rc = action($prog, "diff", @files_to_commit);
} else {
if ($all) {
@files_to_commit = ("-a");
}
my @extra_args = ();
if ($changelog_info) {
@extra_args = ("--author=$maintainer", "--date=$date");
}
if ($signcommit) {
my $sign = '--gpg-sign';
$sign .= "=$keyid" if $keyid;
push(@extra_args, $sign);
}
if ($signoff) {
push(@extra_args, '--signoff');
}
$action_rc = action($prog, "commit", "-m", $message, @extra_args,
@files_to_commit);
}
} elsif ($prog eq 'tla' || $prog eq 'baz') {
my $summary = $message;
$summary =~ s/^((?:\* )?[^\n]{1,72})(?:(?:\s|\n).*|$)/$1/ms;
my @args;
if (!$diffmode) {
if ($summary eq $message) {
$summary =~ s/^\* //s;
@args = ("-s", $summary);
} else {
$summary =~ s/^\* //s;
@args = ("-s", "$summary ...", "-L", $message);
}
}
push(@args, (($prog eq 'tla') ? '--' : ()), @files_to_commit,)
if @files_to_commit;
$action_rc = action($prog, $diffmode ? "diff" : "commit", @args);
} elsif ($prog eq 'bzr') {
if ($diffmode) {
$action_rc = action($prog, "diff", @files_to_commit);
} else {
my @extra_args = bzr_find_fixes($message);
if ($changelog_info) {
eval {
require Date::Format;
require Date::Parse;
};
if ($@) {
my $error
= "$progname: Couldn't format the changelog date: ";
if ($@ =~ m%^Can\'t locate Date%) {
$error
.= "the libtimedate-perl package is not installed";
} else {
$error .= "couldn't load Date::Format/Date::Parse: $@";
}
die "$error\n";
}
my @time = Date::Parse::strptime($date);
my $time
= Date::Format::strftime('%Y-%m-%d %H:%M:%S %z', \@time);
push(@extra_args,
"--author=$maintainer", "--commit-time=$time");
}
my @sign_args;
if ($signcommit) {
push(@sign_args, "-Ocreate_signatures=always");
if ($keyid) {
push(@sign_args, "-Ogpg_signing_key=$keyid");
}
}
$action_rc = action($prog, @sign_args, "commit", "-m", $message,
@extra_args, @files_to_commit);
}
} elsif ($prog eq 'darcs') {
if (!@files_to_commit && ($all || $release)) {
# check to see if the WC is clean. darcs record would exit
# nonzero, so don't run it in --all or --release mode.
$action_rc = action($prog, "status");
if (!$action_rc) {
return;
}
}
if ($diffmode) {
$action_rc = action($prog, "diff", @files_to_commit);
} else {
my $fh = File::Temp->new(TEMPLATE => '.commit-tmp.XXXXXX');
$fh->print("$message\n");
$fh->close();
$action_rc = action($prog, "record", "--logfile", "$fh", "-a",
@files_to_commit);
}
} else {
die "debcommit: unknown program $prog";
}
die "debcommit: commit failed\n" if (!$action_rc);
}
sub tag {
my ($package, $tag, $tag_msg) = @_;
# Make the message here so we can mangle $tag later, if needed
$tag_msg
= !defined $message
? "tagging package $package version $tag"
: "$message";
if ($prog eq 'svn' || $prog eq 'svk') {
my $svnpath = `svnpath`;
chomp $svnpath;
my $tagpath = `svnpath tags`;
chomp $tagpath;
if (!action($prog, "copy", $svnpath, "$tagpath/$tag", "-m", $tag_msg))
{
if (
!action(
$prog, "mkdir", $tagpath, "-m", "create tag directory"
)
|| !action(
$prog, "copy", $svnpath, "$tagpath/$tag",
"-m", $tag_msg
)
) {
die "debcommit: failed tagging with $tag\n";
}
}
} elsif ($prog eq 'cvs') {
$tag =~ s/^[0-9]+://; # strip epoch
$tag =~ tr/./_/; # mangle for cvs
$tag = "debian_version_$tag";
if (!action("cvs", "tag", "-f", $tag)) {
die "debcommit: failed tagging with $tag\n";
}
} elsif ($prog eq 'tla' || $prog eq 'baz') {
my $archpath = `archpath`;
chomp $archpath;
my $tagpath = `archpath releases--\Q$tag\E`;
chomp $tagpath;
my $subcommand;
if ($prog eq 'baz') {
$subcommand = "branch";
} else {
$subcommand = "tag";
}
if (!action($prog, $subcommand, $archpath, $tagpath)) {
die "debcommit: failed tagging with $tag\n";
}
} elsif ($prog eq 'bzr') {
if (action("$prog tags >/dev/null 2>&1")) {
if (!action($prog, "tag", $tag)) {
die "debcommit: failed tagging with $tag\n";
}
} else {
die
"debcommit: bazaar or branch version too old to support tags\n";
}
} elsif ($prog eq 'git') {
$tag =~ tr/~/_/; # mangle for git
$tag =~ tr/:/%/;
if ($tag =~ /-/) {
# not a native package, so tag as a debian release
$tag = "debian/$tag";
}
if ($signtags) {
my $tag_msg = "tagging package $package version $tag";
if (defined $keyid) {
if (
!action(
$prog, "tag", "-a", "-u",
$keyid, "-m", $tag_msg, $tag
)
) {
die "debcommit: failed tagging with $tag\n";
}
} else {
if (!action($prog, "tag", "-a", "-s", "-m", $tag_msg, $tag)) {
die "debcommit: failed tagging with $tag\n";
}
}
} elsif (!action($prog, "tag", "-a", "-m", $tag_msg, $tag)) {
die "debcommit: failed tagging with $tag\n";
}
} elsif ($prog eq 'hg') {
$tag =~ s/^[0-9]+://; # strip epoch
$tag = "debian-$tag";
if (!action($prog, "tag", "-m", $tag_msg, $tag)) {
die "debcommit: failed tagging with $tag\n";
}
} elsif ($prog eq 'darcs') {
if (!action($prog, "tag", $tag)) {
die "debcommit: failed tagging with $tag\n";
}
} else {
die "debcommit: unknown program $prog";
}
}
sub getmessage {
my $ret;
if ($prog =~ /^(cvs|svn|svk|tla|baz|bzr|git|hg|darcs)$/) {
$ret = '';
my @diffcmd;
if ($prog eq 'tla') {
@diffcmd = ($prog, 'diff', '-D', '-w', '--');
} elsif ($prog eq 'baz') {
@diffcmd = ($prog, 'file-diff');
} elsif ($prog eq 'bzr') {
@diffcmd = ($prog, 'diff', '--diff-options', '-wu');
} elsif ($prog eq 'git') {
if (git_repo_has_commits()) {
if ($all) {
@diffcmd
= ('git', 'diff', '--no-ext-diff', '-w', '--no-color');
} else {
@diffcmd = (
'git', 'diff',
'--no-ext-diff', '-w',
'--cached', '--no-color'
);
}
} else {
# No valid head! Rather than fail, cheat and use 'diff'
@diffcmd = ('diff', '-u', '/dev/null');
}
} elsif ($prog eq 'svn') {
@diffcmd = (
$prog, 'diff', '--diff-cmd', '/usr/bin/diff', '--extensions',
'-wu'
);
} elsif ($prog eq 'svk') {
$ENV{'SVKDIFF'} = '/usr/bin/diff -w -u';
@diffcmd = ($prog, 'diff');
} elsif ($prog eq 'darcs') {
@diffcmd = ($prog, 'diff', '--diff-opts=-wu');
if ($onlydebian) {
push(@diffcmd, '--repodir=debian');
}
} else {
@diffcmd = ($prog, 'diff', '-w');
}
open CHLOG, '-|', @diffcmd, $changelog
or die "debcommit: cannot run $diffcmd[0]: $!\n";
foreach (<CHLOG>) {
next unless s/^\+( |\t)//;
next if /^\s*\[.*\]\s*$/; # maintainer name
$ret .= $_;
}
if (!length $ret) {
if ($release) {
return;
} else {
my $info = '';
if ($prog eq 'git') {
$info
= ' (do you mean "debcommit -a" or did you forget to run "git add"?)';
}
die
"debcommit: unable to determine commit message using $prog$info\nTry using the -m flag.\n";
}
} else {
if ($prog =~ /^(git|hg|darcs)$/ and not $diffmode) {
my $count = () = $ret =~ /^\s*[\*\+-] /mg;
if ($count == 1) {
# Unfold
$ret =~ s/\n\s+/ /mg;
} else {
my $summary = '';
# We're constructing a message that can be used as a
# good starting point, the user will need to fine-tune it
$edit = 1;
$summary = $ret;
# Strip off the second and subsequent changes
$summary =~ s/(^\* .*?)^\s*[\*\+-] .*/$1/ms;
# Unfold
$summary =~ s/\n\s+/ /mg;
if ($prog eq 'git') {
$summary =~ s/^\* //;
$ret = $summary . "\n" . $ret;
} else {
# Strip off the first change so that we can prepend
# the unfolded version
$ret =~ s/^\* .*?(^\s*[\*\+-] .*)/$1\n/msg;
$ret = $summary . $ret;
}
}
}
if ($stripmessage or $prog eq 'git') {
my $count = () = $ret =~ /^[ \t]*[\*\+-] /mg;
if ($count == 1) {
$ret =~ s/^[ \t]*[\*\+-] //;
$ret =~ s/^[ \t]*//mg;
}
}
}
} else {
die "debcommit: unknown program $prog";
}
chomp $ret;
return $ret;
}
sub confirm {
my $confirmmessage = shift;
print $confirmmessage, "\n--\n";
while (1) {
print "OK to commit? [Y/n/e] ";
$_ = <STDIN>;
return 0 if /^n/i;
if (/^(y|$)/i) {
$message = $confirmmessage;
return 1;
} elsif (/^e/i) {
($confirmmessage) = edit($confirmmessage);
print "\n", $confirmmessage, "\n--\n";
}
}
}
# The string returned by edit is chomp()ed, so anywhere we present that string
# to the user again needs to have a \n tacked on to the end.
sub edit {
my $message = shift;
my $fh = File::Temp->new(TEMPLATE => '.commit-tmp.XXXXXX')
|| die "$progname: unable to create a temporary file.\n";
# Ensure the message we present to the user has an EOL on the last line.
chomp($message);
$fh->print("$message\n");
$fh->close();
my $mtime = (stat("$fh"))[9];
defined $mtime
|| die
"$progname: unable to retrieve modification time for temporary file: $!\n";
$mtime--;
utime $mtime, $mtime, $fh->filename;
system("sensible-editor $fh");
open(FH, '<', "$fh")
|| die "$progname: unable to open temporary file for reading\n";
$message = "";
while (<FH>) {
$message .= $_;
}
close(FH);
my $newmtime = (stat("$fh"))[9];
defined $newmtime
|| die
"$progname: unable to retrieve modification time for updated temporary file: $!\n";
chomp $message;
return ($message, $mtime != $newmtime);
}
sub git_repo_has_commits {
my $command = "git rev-parse --verify --quiet HEAD >/dev/null";
system $command;
return ($? >> 8 == 0) ? 1 : 0;
}
=head1 LICENSE
This code is copyright by Joey Hess <joeyh@debian.org>, all rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
=head1 AUTHOR
Joey Hess <joeyh@debian.org>
=head1 SEE ALSO
B<debchange>(1), B<svnpath>(1)
=cut

382
scripts/debdiff-apply Executable file
View file

@ -0,0 +1,382 @@
#!/usr/bin/python3
# Copyright (c) 2016-2017, Ximin Luo <infinity0@debian.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See file /usr/share/common-licenses/GPL-3 for more details.
#
# pylint: disable=invalid-name
# pylint: enable=invalid-name
"""
Apply a debdiff to a Debian source package.
It handles d/changelog hunks specially, to avoid conflicts.
Depends on dpkg-dev, devscripts, python3-unidiff, quilt.
"""
import argparse
import email.utils
import hashlib
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import time
try:
import unidiff
except ImportError:
print(
"Please install 'python3-unidiff' in order to use this utility.",
file=sys.stderr,
)
sys.exit(1)
from debian.changelog import ChangeBlock, Changelog
# this can be any valid value, it doesn't appear in the final output
DCH_DUMMY_TAIL = (
"\n -- debdiff-apply dummy tool <infinity0@debian.org> "
"Thu, 01 Jan 1970 00:00:00 +0000\n\n"
)
CHBLOCK_DUMMY_PACKAGE = "debdiff-apply PLACEHOLDER"
TRY_ENCODINGS = ["utf-8", "latin-1"]
DISTRIBUTION_DEFAULT = "experimental"
def workaround_dpkg_865430(dscfile, origdir, stdout):
filename = subprocess.check_output(["dcmd", "--tar", "echo", dscfile]).rstrip()
if not os.path.exists(
os.path.join(origdir.encode("utf-8"), os.path.basename(filename))
):
subprocess.check_call(["dcmd", "--tar", "cp", dscfile, origdir], stdout=stdout)
def is_dch(path):
dirname = os.path.dirname(path)
return (
os.path.basename(path) == "changelog"
and os.path.basename(dirname) == "debian"
and os.path.dirname(os.path.dirname(dirname)) == ""
)
def hunk_lines_to_str(hunk_lines):
return "".join(map(lambda x: str(x)[1:], hunk_lines))
def read_dch_patch(dch_patch):
if len(dch_patch) > 1:
raise ValueError(
"don't know how to deal with debian/changelog patch "
"that has more than one hunk"
)
hunk = dch_patch[0]
source_str = hunk_lines_to_str(hunk.source_lines()) + DCH_DUMMY_TAIL
target_str = hunk_lines_to_str(hunk.target_lines())
# here we assume the debdiff has enough context to see the previous version
# this should be true all the time in practice
source_version = str(Changelog(source_str, 1)[0].version)
target = Changelog(target_str, 1)[0]
return source_version, target
def apply_dch_patch(source_file, current, old_version, target, dry_run):
target_version = str(target.version)
if not old_version or not target_version.startswith(old_version):
logging.warning(
"don't know how to rebase version-change (%s => %s) onto %s",
old_version,
target_version,
old_version,
)
newlog = subprocess.getoutput("EDITOR=cat dch -n 2>/dev/null").rstrip()
version = str(Changelog(newlog, 1)[0].version)
logging.warning(
"using version %s based on `dch -n`; feel free to make me smarter", version
)
else:
version_suffix = target_version[len(old_version) :]
version = str(current[0].version) + version_suffix
logging.info("using version %s based on suffix %s", version, version_suffix)
if dry_run:
return version
current._blocks.insert(0, target) # pylint: disable=protected-access
current.set_version(version)
shutil.copy(source_file, source_file + ".new")
try:
# disable unspecified-encoding, as in Mattia's opinion this should
# likely be rewritten to use pure binary instead of encode/decode.
# pylint: disable=unspecified-encoding
with open(source_file + ".new", "w") as fp:
current.write_to_open_file(fp)
os.rename(source_file + ".new", source_file)
except Exception:
logging.warning("failed to patch %s", source_file)
logging.warning("half-applied changes in %s", source_file + ".new")
logging.warning("current working directory is %s", os.getcwd())
raise
return version
def call_patch(patch_str, *args, check=True, **kwargs):
return subprocess.run(
["patch", "-p1"] + list(args),
input=patch_str,
universal_newlines=True,
check=check,
**kwargs,
)
def check_patch(patch_str, *args, **kwargs):
patch = call_patch(
patch_str,
"--dry-run",
"-f",
"--silent",
*args,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
**kwargs,
)
return patch.returncode == 0
def debdiff_apply(patch, patch_name, args):
# don't change anything if...
dry_run = args.target_version or args.source_version
changelog = list(filter(lambda x: is_dch(x.path), patch))
if not changelog:
logging.info("no debian/changelog in patch: %s", args.patch_file)
old_version = None
target = ChangeBlock(
package=CHBLOCK_DUMMY_PACKAGE,
author=f"{os.getenv('DEBFULLNAME')} <{os.getenv('DEBEMAIL')}>",
date=email.utils.formatdate(time.time(), localtime=True),
version=None,
distributions=args.distribution,
urgency="low",
changes=["", f" * Rebase patch {patch_name}.", ""],
)
target.add_trailing_line("")
elif len(changelog) > 1:
raise ValueError("more than one debian/changelog patch???")
else:
patch.remove(changelog[0])
old_version, target = read_dch_patch(changelog[0])
if args.source_version:
if old_version:
print(old_version)
return False
# read this here so --source-version can work even without a d/changelog
with open(args.changelog, encoding="utf8") as fp:
current = Changelog(fp.read())
if target.package == CHBLOCK_DUMMY_PACKAGE:
target.package = current[0].package
if not dry_run:
patch_str = str(patch)
if check_patch(patch_str, "-N"):
call_patch(patch_str)
logging.info("patch %s applies!", patch_name)
elif check_patch(patch_str, "-R"):
logging.warning("patch %s already applied", patch_name)
return False
else:
call_patch(patch_str, "--dry-run", "-f")
raise ValueError(f"patch {patch_name} doesn't apply!")
# only apply d/changelog patch if the rest of the patch applied
new_version = apply_dch_patch(args.changelog, current, old_version, target, dry_run)
if args.target_version:
print(new_version)
return False
if args.repl:
import code # pylint: disable=import-outside-toplevel
code.interact(local=locals())
return True
def parse_args(args):
parser = argparse.ArgumentParser(
description="Apply a debdiff to a Debian source package"
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Output more information"
)
parser.add_argument(
"-c",
"--changelog",
default="debian/changelog",
help="Path to debian/changelog; default: %(default)s",
)
parser.add_argument(
"-D",
"--distribution",
default="experimental",
help="Distribution to use, if the patch doesn't already "
"contain a changelog; default: %(default)s",
)
parser.add_argument(
"--repl", action="store_true", help="Run the python REPL after processing."
)
parser.add_argument(
"--source-version",
action="store_true",
help="Don't apply the patch; instead print out the version of the "
"package that it is supposed to be applied to, or nothing if "
"the patch does not specify a source version.",
)
parser.add_argument(
"--target-version",
action="store_true",
help="Don't apply the patch; instead print out the new version of the "
"package debdiff-apply(1) would generate, when the patch is applied to the "
"the given target package, as specified by the other arguments.",
)
parser.add_argument(
"orig_dsc_or_dir",
nargs="?",
default=".",
help="Target to apply the patch to. This can either be an unpacked "
"source tree, or a .dsc file. In the former case, the directory is "
"modified in-place; in the latter case, a second .dsc is created. "
"Default: %(default)s",
)
parser.add_argument(
"patch_file",
nargs="?",
default="/dev/stdin",
help="Patch file to apply, in the format output by debdiff(1)."
" Default: %(default)s",
)
group1 = parser.add_argument_group("Options for .dsc patch targets")
group1.add_argument(
"--no-clean",
action="store_true",
help="Don't clean temporary directories after a failure, so you can "
"examine what failed.",
)
group1.add_argument(
"--quilt-refresh",
action="store_true",
help="If the building of the new source package fails, try to refresh "
"patches using quilt(1) then try building it again.",
)
group1.add_argument(
"-d",
"--directory",
default=None,
help="Extract the .dsc into this directory, which won't be cleaned up "
"after debdiff-apply(1) exits. If not given, then it will be extracted to a "
"temporary directory.",
)
return parser.parse_args(args)
def main(args):
# Split this function!
# pylint: disable=too-many-branches,too-many-locals,too-many-statements
args = parse_args(args)
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
with open(args.patch_file, "rb") as fp:
data = fp.read()
for enc in TRY_ENCODINGS:
try:
patch = unidiff.PatchSet(data.splitlines(keepends=True), encoding=enc)
break
except Exception: # pylint: disable=broad-except
if enc == TRY_ENCODINGS[-1]:
raise
continue
hex_digest = hashlib.sha256(data).hexdigest()[
: 20 if args.patch_file == "/dev/stdin" else 8
]
patch_name = f"{os.path.basename(args.patch_file)}:{hex_digest}"
quiet = args.source_version or args.target_version
dry_run = args.source_version or args.target_version
# user can redirect stderr themselves
stdout = subprocess.DEVNULL if quiet else None
# change directory before applying patches
if os.path.isdir(args.orig_dsc_or_dir):
os.chdir(args.orig_dsc_or_dir)
debdiff_apply(patch, patch_name, args)
elif os.path.isfile(args.orig_dsc_or_dir):
dscfile = args.orig_dsc_or_dir
parts = os.path.splitext(os.path.basename(dscfile))
if parts[1] != ".dsc":
raise ValueError(f"unrecognised patch target: {dscfile}")
extractdir = args.directory if args.directory else tempfile.mkdtemp()
if not os.path.isdir(extractdir):
os.makedirs(extractdir)
try:
# dpkg-source doesn't like existing dirs
builddir = os.path.join(extractdir, parts[0])
subprocess.check_call(
["dpkg-source", "-x", "--skip-patches", dscfile, builddir],
stdout=stdout,
)
origdir = os.getcwd()
workaround_dpkg_865430(dscfile, origdir, stdout)
os.chdir(builddir)
did_patch = debdiff_apply(patch, patch_name, args)
if dry_run or not did_patch:
return
os.chdir(origdir)
try:
subprocess.check_call(["dpkg-source", "-b", builddir])
except subprocess.CalledProcessError:
if args.quilt_refresh:
subprocess.check_call(
[
"sh",
"-c",
"""
set -ex
export QUILT_PATCHES=debian/patches
while quilt push; do quilt refresh; done
""",
],
cwd=builddir,
)
subprocess.check_call(["dpkg-source", "-b", builddir])
else:
raise
finally:
cleandir = builddir if args.directory else extractdir
if args.no_clean:
logging.warning("you should clean up temp files in %s", cleandir)
else:
shutil.rmtree(cleandir)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

112
scripts/debdiff-apply.1 Normal file
View file

@ -0,0 +1,112 @@
.\" Copyright (c) 2016-2017, Ximin Luo <infinity0@debian.org>
.\"
.\" This program is free software; you can redistribute it and/or
.\" modify it under the terms of the GNU General Public License
.\" as published by the Free Software Foundation; either version 3
.\" of the License, or (at your option) any later version.
.\"
.\" This program is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" See file /usr/share/common-licenses/GPL-3 for more details.
.\"
.TH "DEBDIFF\-APPLY" 1 "Debian Utilities" "DEBIAN"
.SH NAME
debdiff-apply \- apply a debdiff to a Debian source package
.SH SYNOPSIS
.B debdiff-apply
[options] [orig_dsc_or_dir] [patch_file]
.br
.B debdiff-apply
[options] < [patch_file]
.SH DESCRIPTION
.B debdiff-apply
takes a \fIpatchfile\fR that describes the differences between two Debian
source packages \fIold\fR and \fInew\fR, and applies it to a target Debian
source package \fIorig\fR.
.PP
\fIorig\fR could either be the same as \fIold\fR or it could be different.
\fIpatchfile\fR is expected to be a unified diff between two Debian source
trees, as what
.BR debdiff (1)
normally generates.
.PP
Any changes to \fIdebian/changelog\fR are dealt with specially, to avoid the
conflicts that changelog diffs typically produce when applied naively. The
exact behaviour may be tweaked in the future, so one should not rely on it.
.PP
If \fIpatchfile\fR does not apply to \fIorig\fR, even after the special-casing
of \fIdebian/changelog\fR, no changes are made and
.BR debdiff-apply (1)
will exit with a non-zero error code.
.SH ARGUMENTS
.TP
orig_dsc_or_dir
Target to apply the patch to. This can either be an unpacked source tree, or a
\[char46]dsc file. In the former case, the directory is modified in\-place; in
the latter case, a second .dsc is created. Default: \fI.\fP
.TP
patch_file
Patch file to apply, in the format output by
.BR debdiff (1).
Default:
\fI\,/dev/stdin\/\fP
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
show this help message and exit
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Output more information
.TP
\fB\-c\fR CHANGELOG, \fB\-\-changelog\fR CHANGELOG
Path to debian/changelog; default: debian/changelog
.TP
\fB\-D\fR DISTRIBUTION, \fB\-\-distribution\fR DISTRIBUTION
Distribution to use, if the patch doesn't already contain a changelog; default:
experimental
.TP
\fB\-\-repl\fR
Run the python REPL after processing.
.TP
\fB\-\-source\-version\fR
Don't apply the patch; instead print out the version of the package that it is
supposed to be applied to, or nothing if the patch does not specify a source
version.
.TP
\fB\-\-target\-version\fR
Don't apply the patch; instead print out the new version of the package
.BR debdiff-apply (1)
would generate, when the patch is applied to the the given target
package, as specified by the other arguments.
.SS "For .dsc patch targets:"
.TP
\fB\-\-no\-clean\fR
Don't clean temporary directories after a failure, so you can examine what
failed.
.TP
\fB\-\-quilt\-refresh\fR
If the building of the new source package fails, try to refresh patches using
.BR quilt (1)
then try building it again.
.TP
\fB\-d\fR DIRECTORY, \fB\-\-directory\fR DIRECTORY
Extract the .dsc into this directory, which won't be cleaned up after
.BR debdiff-apply (1)
exits. If not given, then it will be extracted to a temporary directory.
.SH AUTHORS
\fBdebdiff-apply\fR and this manual page were written by Ximin Luo
<infinity0@debian.org>
.PP
Both are released under the GNU General Public License, version 3 or later.
.SH SEE ALSO
.BR debdiff (1)

264
scripts/debdiff.1 Normal file
View file

@ -0,0 +1,264 @@
.TH DEBDIFF 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debdiff \- compare file lists in two Debian packages
.SH SYNOPSIS
\fBdebdiff\fR [\fIoptions\fR] \fR
.br
\fBdebdiff\fR [\fIoptions\fR] ... \fIdeb1 deb2\fR
.br
\fBdebdiff\fR [\fIoptions\fR] ... \fIchanges1 changes2\fR
.br
\fBdebdiff\fR [\fIoptions\fR] ... \fB\-\-from \fIdeb1a deb1b ...
\fB\-\-to \fIdeb2a deb2b ...\fR
.br
\fBdebdiff\fR [\fIoptions\fR] ... \fIdsc1 dsc2\fR
.SH DESCRIPTION
\fBdebdiff\fR takes the names of two Debian package files (\fI.deb\fRs
or \fI.udeb\fRs) on the command line and compares their contents
(considering only the files in the main package, not the maintenance
scripts). It shows which files have been introduced and which removed
between the two package files, and is therefore useful for spotting
files which may have been inadvertently lost between revisions of the
package. It also checks the file owners and permissions, and compares
the control files of the two packages using the \fBwdiff\fR program.
If you want a deeper comparison of two Debian package files you can
use the \fBdiffoscope\fR tool.
.PP
If no arguments are given, \fBdebdiff\fR tries to compare the content
of the current source directory with the last version of the package.
.PP
\fBdebdiff\fR can also handle changes between groups of \fI.deb\fR
files in two ways. The first is to specify two \fI.changes\fR files.
In this case, the \fI.deb\fR files listed in the \fI.changes\fR file
will be compared, by taking the contents of all of the
listed \fI.deb\fR files together. (The \fI.deb\fR files listed are
assumed to be in the same directory as the \fI.changes\fR file.) The
second way is to list the \fI.deb\fR files of interest specifically
using the \fB\-\-from\fR ... \fB\-\-to\fR syntax. These both help if
a package is broken up into smaller packages and one wishes to ensure
that nothing is lost in the interim.
.PP
\fBdebdiff\fR examines the \fBdevscripts\fR configuration files as
described below. Command line options override the configuration file
settings, though.
.PP
If \fBdebdiff\fR is passed two source packages (\fI.dsc\fR files) it
will compare the contents of the source packages. If the source
packages differ only in Debian revision number (that is,
the \fI.orig.tar.gz\fR files are the same in the two \fI.dsc\fR
files), then \fBinterdiff\fR(1) will be used to compare the two patch
files if this program is available on the system, otherwise a
\fBdiff\fR will be performed between the two source trees.
.SH OPTIONS
.TP
.BR \-\-dirs ", " \-d
The default mode of operation is to ignore directory names which
appear in the file list, but they, too, will be considered if this
option is given.
.TP
.B \-\-nodirs
Ignore directory names which appear in the file list. This is the
default and it can be used to override a configuration file setting.
.TP
.BI \-\-move " FROM TO" "\fR,\fP \-m" " FROM TO"
It sometimes occurs that various files or directories are moved around
between revisions. This can be handled using this option. There are
two arguments, the first giving the location of the directory or file
in the first package, and the second in the second. Any files in the
first listing whose names begin with the first argument are treated as
having that substituted for the second argument when the file lists
are compared. Any number of \fB\-\-move\fR arguments may be given;
they are processed in the order in which they appear. This only affects
comparing binary packages, not source packages.
.TP
.BI \-\-move\-regex " FROM TO"
This is the same as \fB\-\-move\fR, except that \fIFROM\fR is treated
as a regular expression and the \fBperl\fR substitution command
\fIs/^FROM/TO/\fR is applied to the files. In particular, TO can make
use of backreferences such as $1.
.TP
.B \-\-nocontrol
\fBdebdiff\fR will usually compare the respective control files of the
packages using \fBwdiff\fR(1). This option suppresses this part of
the processing.
.TP
.B \-\-control
Compare the respective control files; this is the default, and it can
be used to override a configuration file setting.
.TP
.BI \-\-controlfiles " FILE\fR[\fP", "FILE\fR ...]\fP"
Specify which control files to compare; by default this is just
\fIcontrol\fR, but could include \fIpostinst\fR, \fIconfig\fR and so
on. Files will only be compared if they are present in both
\fI.debs\fR being compared. The special value \fIALL\fR compares all
control files present in both packages, except for md5sums. This
option can be used to override a configuration file setting.
.TP
.B \-\-wdiff\-source\-control
When processing source packages, compare control files using \fBwdiff\fR.
Equivalent to the \fB\-\-control\fR option for binary packages.
.TP
.B \-\-no\-wdiff\-source\-control
Do not compare control files in source packages using \fBwdiff\fR. This
is the default.
.TP
.BR \-\-wp ", " \-\-wl ", " \-\-wt
Pass a \fB\-p\fR, \fB\-l\fR or \fB\-t\fR option to \fBwdiff\fR
respectively. (This yields the whole \fBwdiff\fR output rather than
just the lines with any changes.)
.TP
.B \-\-show-moved
If multiple \fI.deb\fR files are specified on the command line, either
using \fI.changes\fR files or the \fB\-\-from\fR/\fB\-\-to\fR syntax,
then this option will also show which files (if any) have moved
between packages. (The package names are simply determined from the
names of the \fI.deb\fR files.)
.TP
.B \-\-noshow-moved
The default behaviour; can be used to override a configuration file
setting.
.TP
.BI \-\-renamed " FROM TO"
If \fB\-\-show-moved\fR is being used and a package has been renamed
in the process, this command instructs \fBdebdiff\fR to treat the
package in the first list called \fIFROM\fR as if it were called
\fITO\fR. Multiple uses of this option are permitted.
.TP
.BI \-\-exclude " PATTERN"
Exclude files whose basenames match \fIPATTERN\fR.
Multiple uses of this option are permitted.
Note that this option is passed on to \fBdiff\fR and has the same
behaviour, so only the basename of the file is considered:
in particular, \fB--exclude='*.patch'\fR will work, but
\fB--exclude='debian/patches/*'\fR will have no practical effect.
.TP
.B \-\-diffstat
Include the result of \fBdiffstat\fR before the generated diff.
.TP
.B \-\-no\-diffstat
The default behaviour; can be used to override a configuration file
setting.
.TP
.B \-\-auto\-ver\-sort
When comparing source packages, do so in version order.
.TP
.B \-\-no\-auto\-ver\-sort
Compare source packages in the order they were passed on the
command-line, even if that means comparing a package with a higher
version against one with a lower version. This is the default
behaviour.
.TP
.B \-\-unpack\-tarballs
When comparing source packages, also unpack tarballs found in the top level
source directory to compare their contents along with the other files.
This is the default behaviour.
.TP
.B \-\-no\-unpack\-tarballs
Do not unpack tarballs inside source packages.
.TP
.B \-\-apply\-patches
If the old and/or new package is in 3.0 (quilt) format, apply the
quilt patches (and remove \fB.pc/\fR) before comparison.
.TP
.B \-\-no\-apply\-patches, \-\-noapply\-patches
If the old and/or new package is in 3.0 (quilt) format, do not apply the
quilt patches before comparison. This is the default behaviour.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
\fB\-\-debs\-dir\fR \fIdirectory\fR
Look for the \fI.dsc\fR files in \fIdirectory\fR
instead of the parent of the source directory. This should
either be an absolute path or relative to the top of the source
directory.
.TP
.BR \-\-help ", " \-h
Show a summary of options.
.TP
.BR \-\-version ", " \-v
Show version and copyright information.
.TP
.BR \-\-quiet ", " \-q
Be quiet if no differences were found.
.TP
.BR \-\-ignore\-space ", " \-w
Ignore whitespace in diffs.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced by a shell in that order to set
configuration variables. Command line options can be used to override
configuration file settings. Environment variable settings are
ignored for this purpose. The currently recognised variables are:
.TP
.B DEBDIFF_DIRS
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-dirs\fR command line parameter being used.
.TP
.B DEBDIFF_CONTROL
If this is set to \fIno\fR, then it is the same as the
\fB\-\-nocontrol\fR command line parameter being used. The default is
\fIyes\fR.
.TP
.B DEBDIFF_CONTROLFILES
Which control files to compare, corresponding to the
\fB\-\-controlfiles\fR command line option. The default is
\fIcontrol\fR.
.TP
.B DEBDIFF_SHOW_MOVED
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-show\-moved\fR command line parameter being used.
.TP
.B DEBDIFF_WDIFF_OPT
This option will be passed to \fBwdiff\fR; it should be one of
\fB\-p\fR, \fB\-l\fR or \fB\-t\fR.
.TP
.B DEBDIFF_SHOW_DIFFSTAT
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-diffstat\fR command line parameter being used.
.TP
.B DEBDIFF_WDIFF_SOURCE_CONTROL
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-wdiff\-source\-control\fR command line parameter being used.
.TP
.B DEBDIFF_AUTO_VER_SORT
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-auto\-ver\-sort\fR command line parameter being used.
.TP
.B DEBDIFF_UNPACK_TARBALLS
If this is set to \fIno\fR, then it is the same as the
\fB\-\-no\-unpack\-tarballs\fR command line parameter being used.
.TP
.B DEBDIFF_APPLY_PATCHES
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-apply\-patches\fR command line parameter being used.
The default is \fIno\fR.
.TP
.B DEBRELEASE_DEBS_DIR
This specifies the directory in which to look for the \fI.dsc\fR
and files, and is either an absolute path or relative to
the top of the source tree. This corresponds to the
\fB\-\-debs\-dir\fR command line option. This directive could be
used, for example, if you always use \fBpbuilder\fR or
\fBsvn-buildpackage\fR to build your packages. Note that it also
affects \fBdebrelease\fR(1) in the same way, hence the strange name of
the option.
.SH "EXIT VALUES"
Normally the exit value will be 0 if no differences are reported and 1
if any are reported. If there is some fatal error, the exit code will
be 255.
.SH "SEE ALSO"
.BR debdiff-apply (1),
.BR diffstat (1),
.BR dpkg-deb (1),
.BR interdiff (1),
.BR wdiff (1),
.BR devscripts.conf (5),
.BR diffoscope (1)
.SH AUTHOR
\fBdebdiff\fR was originally written as a shell script by Yann Dirson
<dirson@debian.org> and rewritten in Perl with many more features by
Julian Gilbey <jdg@debian.org>. The software may be freely
redistributed under the terms and conditions of the GNU General Public
License, version 2.

View file

@ -0,0 +1,154 @@
# /usr/share/bash-completion/completions/debdiff
# Bash command completion for debdiff(1).
# Documentation: bash(1), section “Programmable Completion”.
# This is free software, and you are welcome to redistribute it under
# certain conditions; see the end of this file for copyright
# information, grant of license, and disclaimer of warranty.
_have debdiff &&
_debdiff () {
local cur prev words cword
_init_completion || return
local i
local command_name=debdiff
local options=(
-h --help -v --version
-q --quiet
-d --dirs --nodirs
-w --ignore-space
--diffstat --no-diffstat
--auto-ver-sort --no-auto-ver-sort
--unpack-tarballs --no-unpack-tarballs
--apply-patches --no-apply-patches
--control --nocontrol --controlfiles
--wdiff-source-control --no-wdiff-source-control --wp --wl --wt
--show-moved --noshow-moved --renamed
--debs-dir
--from
--move --move-regex
--exclude
)
local file_list_mode=normal
local -i move_from=-1
local -i move_to=-1
unset COMPREPLY
case "$prev" in
"$command_name")
options+=( --noconf --no-conf )
;;
--debs-dir)
COMPREPLY=( $( compgen -A directory -- "$cur" ) )
;;
esac
if [[ -v COMPREPLY ]] ; then
return 0
fi
for (( i=1; i<${#words[@]}; i++ )); do
if [[ $file_list_mode == @(deb|dsc|changes) ]]; then
if (( i == ${#words[@]}-1 )); then
break
else
COMPREPLY=()
return 0
fi
fi
if (( ${move_from} == -1 && ${move_to} == -1 )); then
file_list_mode=normal
elif (( ${move_from} >= 0 && ${move_to} == -1 )); then
file_list_mode=from
elif (( ${move_from} >= 0 && ${move_to} >= 0 && ${move_to} < ${move_from} )); then
file_list_mode=to
else
COMPREPLY=()
return 0
fi
if [[ $file_list_mode == normal && ${words[i]} == --from ]]; then
move_from=0
file_list_mode=from
elif [[ $file_list_mode == normal && ${words[i]} == *.deb ]]; then
file_list_mode=deb
elif [[ $file_list_mode == normal && ${words[i]} == *.udeb ]]; then
file_list_mode=deb
elif [[ $file_list_mode == normal && ${words[i]} == *.dsc ]]; then
file_list_mode=dsc
elif [[ $file_list_mode == normal && ${words[i]} == *.changes ]]; then
file_list_mode=changes
elif [[ $file_list_mode == from && ${words[i]} == *.deb ]]; then
(( ++move_from ))
elif [[ $file_list_mode == from && ${words[i]} == *.udeb ]]; then
(( ++move_from ))
elif [[ $file_list_mode == from && ${words[i]} == --to ]]; then
move_to=0
file_list_mode=to
elif [[ $file_list_mode = to && ${words[i]} == *.deb ]]; then
(( ++move_to ))
elif [[ $file_list_mode = to && ${words[i]} == *.udeb ]]; then
(( ++move_to ))
fi
done
case $file_list_mode in
normal)
if [[ $prev == --debs-dir ]]; then
COMPREPLY=( $( compgen -G "${cur}*" ) )
compopt -o dirnames
elif [[ $cur == -* ]]; then
COMPREPLY=( $( compgen -W "${options[*]}" -- "$cur" ) )
else
COMPREPLY=( $( compgen -G "${cur}*.@(deb|udeb|dsc|changes)" ) )
compopt -o filenames
compopt -o plusdirs
fi
;;
deb|from|to)
COMPREPLY=( $( compgen -G "${cur}*.deb" "${cur}*.udeb" ) )
if (( $move_from > 0 && $move_to < 0 )) ; then
COMPREPLY+=( $( compgen -W "--to" -- "$cur" ) )
fi
compopt -o filenames
compopt -o plusdirs
;;
dsc)
COMPREPLY=( $( compgen -G "${cur}*.dsc" ) )
compopt -o filenames
compopt -o plusdirs
;;
changes)
COMPREPLY=( $( compgen -G "${cur}*.changes" ) )
compopt -o filenames
compopt -o plusdirs
;;
*)
COMPREPLY=( $( compgen -W "${options[*]}" -- "$cur" ) )
;;
esac
return 0
} &&
complete -F _debdiff debdiff
# Copyright © 20162017 Ben Finney <ben+debian@benfinney.id.au>
# Copyright © 2015 Nicholas Bamber <nicholas@periapt.co.uk>
#
# This is free software: you may copy, modify, and/or distribute this work
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 2 of that license or any later version.
# No warranty expressed or implied. See the file LICENSE.GPL-2 for details.
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

1258
scripts/debdiff.pl Executable file

File diff suppressed because it is too large Load diff

389
scripts/debftbfs Executable file
View file

@ -0,0 +1,389 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# Copyright 2024 Johannes Schauer Marin Rodrigues <josch@debian.org>
import argparse
import csv
import json
import logging
import shutil
import subprocess
import tempfile
import urllib.request
from collections import defaultdict
from itertools import islice
from pathlib import Path
from debian.deb822 import Sources
from debian.debian_support import Version
# FIXME: replace with itertools.batched once python 3.12 is in Debian stable
def batched(iterable, num):
# batched('ABCDEFG', 3) → ABC DEF G
if num < 1:
raise ValueError("n must be at least one")
iterator = iter(iterable)
while batch := tuple(islice(iterator, num)):
yield batch
def process_data(bugnum, source, affects, title):
yield f"{source} {bugnum} {title}"
for affected in affects:
if not affected.startswith("src:"):
continue
source = affected.removeprefix("src:")
yield f"{source} {bugnum} {title}"
def run_psql(dist):
# the below replicates the sql command used by the udd cgi
# it uses bugs instead of all_bugs as we are not interested in archived bugs
# it uses the bugs_rt_affects_unstable table instead of the affects_unstable
# column as the former has all the release team logic about xx-ignore BTS tags
# the severity column is an enum so we can compare with greater-equal
query = f"""
COPY (SELECT id, source, affected_packages, title
FROM bugs
where id in (select id from bugs_rt_affects_{dist})
and id in (select id from bugs_tags where tag='ftbfs')
and not (id not in (select id from bugs_packages, sources
where bugs_packages.source = sources.source and component='main'))
AND (severity >= 'serious')
) TO STDOUT
"""
csv_data = subprocess.check_output(
[
"psql",
"--host=udd-mirror.debian.net",
"--user=udd-mirror",
"udd",
"-c",
query,
],
env={"PGPASSWORD": "udd-mirror"},
# encoding="UTF-8",
text=True,
)
output = []
for bugnum, pkg, affects, title in csv.reader(
csv_data.splitlines(), delimiter="\t"
):
output.extend(
process_data(
bugnum, pkg, affects.split(","), title.encode("latin-1").decode("utf-8")
)
)
print("\n".join(sorted(output)))
def run_json(dist):
# the 'release' argument requires codenames, not suite names
codename = None
with urllib.request.urlopen(
f"http://deb.debian.org/debian/dists/{dist}/Release"
) as f:
for line in f:
if not line.startswith(b"Codename: "):
continue
line = line.removeprefix(b"Codename: ")
codename = line.rstrip(b"\n").decode()
break
if codename is None:
raise ValueError(f"unable to find Codename field in Release file for {dist}")
with urllib.request.urlopen(
f"https://udd.debian.org/bugs/?release={codename}&ftbfs=only"
"&notmain=ign&merged=&fnewerval=7&flastmodval=7&rc=1&sortby=id"
"&caffected_packages=1&sorto=asc&format=json"
) as response:
output = []
for entry in json.load(response):
bugnum = entry["id"]
pkg = entry["source"]
affects = entry["affected_packages"].split(",")
title = (
entry["title"]
.encode("latin-1")
.decode("utf-8")
.replace("\\", "\\\\")
.replace("\r", "\\r")
.replace("\n", "\\n")
.replace("\t", "\\t")
)
output.extend(process_data(bugnum, pkg, affects, title))
print("\n".join(sorted(output)))
def soap_check_vers(verlist, source, package, sources, affects, src2ver, bin2src):
for ver in verlist:
if "/" in ver:
# best case scenario: the version comes
# with its associated source package name
src, fver = ver.split("/")
elif source and "," not in source:
# if it does not, maybe the "source" field is set
src = source
fver = ver
elif len(sources) == 1:
# if it is not, maybe there is only a single
# source package in the package field
src = next(iter(sources))
fver = ver
elif len(bin2src.get(package, [])) == 1:
# if it is not, maybe the binary package can be
# mapped to a single source package
src = next(iter(bin2src[package]))
fver = ver
elif len(affects) == 1:
# if it is not, maybe there is only a single
# affected package
src = next(iter(affects))
fver = ver
else:
# otherwise, we give up
continue
if src in src2ver and src2ver[src] >= Version(fver):
return True
return False
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
def run_soap(dist):
# Do not import debianbts in the toplevel so that this script can be used
# with the other data sources without having to install python3-debianbts
# pylint: disable=import-outside-toplevel,import-error
import debianbts
src2ver = {}
bin2src = defaultdict(set)
with tempfile.TemporaryDirectory(prefix="debftbfs") as tmpdir:
# download a Sources file from a mirror
subprocess.check_call(
["chdist", "--data-dir", tmpdir, "create", "debftbfs"],
stdout=subprocess.DEVNULL,
)
Path(f"{tmpdir}/debftbfs/etc/apt/sources.list").write_text(
f"deb-src http://deb.debian.org/debian/ {dist} main", encoding="utf-8"
)
subprocess.check_call(
["chdist", "--data-dir", tmpdir, "debftbfs", "apt-get", "update"],
stdout=subprocess.DEVNULL,
)
sources_fname = subprocess.check_output(
[
"chdist",
"--data-dir",
tmpdir,
"debftbfs",
"apt-get",
"indextargets",
"Component: main",
"Created-By: Sources",
f"Suite: {dist}",
"--format",
"$(FILENAME)",
]
)
# fill bin2src and src2ver dicts
with open(sources_fname.removesuffix(b"\n"), encoding="utf-8") as sources:
for src in Sources.iter_paragraphs(sources):
for binpkg in src["Binary"].split(","):
bin2src[binpkg.strip()].add(src["Package"])
ver = Version(src["Version"])
# when there are multiple versions of the source package, only
# keep the highest version
if src["Package"] in src2ver and src2ver[src["Package"]] > ver:
continue
src2ver[src["Package"]] = ver
output = set()
ftbfs_bugs = sorted(
debianbts.get_bugs(
tag="ftbfs",
severity=("critical", "grave", "serious"),
# In rare cases, even archived bugs can affect packages, skipping
# them is a heuristic
archive="0",
)
)
for batch in batched(ftbfs_bugs, 64):
for bug in debianbts.get_status(batch):
# We must not exclude bugs that are done because they might be
# fixed in one suite but still affect another.
# if bug.done:
# continue
# Guess the assigned source packages to find out whether the
# version of the source package in the chosen distro is affected.
# As bugs are allowed to carry information which does not align
# with the archive contents, this is a heuristic.
sources = set()
for pkg in bug.package.split(","):
pkg = pkg.strip()
if pkg.startswith("src:"):
sources.add(pkg.removeprefix("src:"))
elif len(bin2src.get(pkg, [])) == 1:
sources.add(next(iter(bin2src[pkg])))
affects = set()
for pkg in bug.affects:
if pkg.startswith("src:"):
affects.add(pkg.removeprefix("src:"))
elif len(bin2src.get(pkg, [])) == 1:
affects.add(next(iter(bin2src[pkg])))
# Guess whether the bug is supposedly found
found = False
# if the bug has no documented found versions, consider it
# found in this suite
if not bug.found_versions:
found = True
if soap_check_vers(
bug.found_versions,
bug.source,
bug.package,
sources,
affects,
src2ver,
bin2src,
):
found = True
# we are only interested in bugs where the version in the
# given distribution is equal or greater than the version
# in which the bug was found
if not found:
logging.debug("skipping %s as it is not found", bug.bug_num)
continue
# Guess whether the bug is supposedly fixed
fixed = False
if soap_check_vers(
bug.fixed_versions,
bug.source,
bug.package,
sources,
affects,
src2ver,
bin2src,
):
fixed = True
# we are only interested in bugs where the version in the
# given distribution is less than the version in which
# the bug was fixed
if fixed:
logging.debug("skipping %s as it is fixed", bug.bug_num)
continue
# in case a binary package is associated with multiple source
# packages, the source field has to be split
for src in [
src for src in bug.package.split(",") if src.startswith("src:")
] + (bug.source.split(",") if bug.source else []):
src = src.strip().removeprefix("src:")
if src not in src2ver:
continue
output.update(
process_data(
bug.bug_num,
src,
bug.affects,
bug.subject.replace("\\", "\\\\")
.replace("\r", "\\r")
.replace("\n", "\\n")
.replace("\t", "\\t"),
)
)
print("\n".join(sorted(output)))
def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
Query udd or bugs.debian.org for all source packages which currently have FTBFS
bugs filed against them for a given distribution (default: unstable) in "main".
If the psql utility is installed, the data will be queried from
udd-mirror.debian.net. Otherwise it will come from udd.debian.org. Querying
the former is a bit faster but the data might be not 100% up to date. The
result is a line-based list of source package names, their FTBFS bug and the
bug title, separated by a space. Only bugs tagged 'ftbfs' and with a severity
greater or equal to 'serious' are selected. The result is sorted by source
package name, alphabetically ascending.
The three available data sources differ in how fast it is to retrieve the data,
how up-to-date the data is and in the chosen heuristic to decide which ftbfs
bugs affect which source package in a given distribution. The fastest two
data sources are udd-mirror.debian.net and udd.debian.org with the former being
around 20% faster than the latter but either finish in under a second. The
former will be more outdated than the latter though at is is only a mirror.
The slowest method is directly querying bugs.debian.org via its SOAP interface.
While querying bugs.debian.org will retrieve the most up-to-date information,
the heuristics chosen to decide whether a source package is affected by an
ftbfs bug slightly differs to the heuristic used by udd. Differences mostly
occur for bugs that were re-assigned to different source packages or have
otherwise missing or incorrect metadata stored.
""",
)
parser.add_argument(
"-d",
"--debug",
help="Enable output of debugging messages",
action="store_const",
dest="loglevel",
const=logging.DEBUG,
default=logging.WARNING,
)
parser.add_argument(
"-v",
"--verbose",
help="Print verbose output",
action="store_const",
dest="loglevel",
const=logging.INFO,
)
parser.add_argument(
"--distribution",
help="Pick the distribution affected by the FTBFS issue",
choices=["stable", "testing", "unstable", "experimental"],
default="unstable",
type=str,
)
parser.add_argument(
"--source",
help=(
"Choose the UDD source between udd-mirror.debian.net, "
"udd.debian.org and bugs.debian.org."
),
choices=["auto", "udd-mirror.d.n", "udd.d.o", "bugs.d.o"],
default="auto",
type=str,
)
args = parser.parse_args()
logging.basicConfig(level=args.loglevel)
match args.source:
case "auto":
# The default method is querying udd-mirror.debian.net with psql
# because
# - it's about 20% faster than downloading json from udd.debian.org.
# - precise control over SQL statement
# - no suite to codename translation required
# The method has the disadvantage that the data might be a
# outdated and that the psql utility needs to be installed.
if shutil.which("psql") is not None:
run_psql(args.distribution)
else:
run_json(args.distribution)
case "bugs.d.o":
run_soap(args.distribution)
case "udd-mirror.d.n":
run_psql(args.distribution)
case "udd.d.o":
run_json(args.distribution)
if __name__ == "__main__":
main()

138
scripts/debi.1 Normal file
View file

@ -0,0 +1,138 @@
.TH DEBI 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debi \- install current version of generated Debian package
.SH SYNOPSIS
\fBdebi\fP [\fIoptions\fR] [\fIchanges file\fR] [\fIpackage\fR ...]
.SH DESCRIPTION
\fBdebi\fR figures out the current version of a package and installs
it. If a \fI.changes\fR file is specified on the command line, the
filename must end with \fI.changes\fR, as this is how the program
distinguishes it from package names. If not, then \fBdebi\fR has to
be called from within the source code directory tree. In this case,
it will look for the \fI.changes\fR file corresponding to the current
package version (by determining the name and version number from the
changelog, and the architecture in the same way as
\fBdpkg-buildpackage\fR(1) does). It then runs \fBdpkg \-i\fR on
every \fI.deb\fR archive listed in the \fI.changes\fR file to install
them, assuming that all of the \fI.deb\fR archives live in the same
directory as the \fI.changes\fR file. Note that you probably don't
want to run this program on a \fI.changes\fR file relating to a
different architecture after cross-compiling the package!
.PP
If a list of packages is given on the command line, then only those
debs with names in this list of packages will be installed.
.PP
Since installing a package requires root privileges, \fBdebi\fR will
only be useful if it is either being run as root or \fBdpkg\fR can
be run as root.
.SH "Directory name checking"
In common with several other scripts in the \fBdevscripts\fR package,
\fBdebi\fR will climb the directory tree until it finds a
\fIdebian/changelog\fR file. As a safeguard against stray files
causing potential problems, it will examine the name of the parent
directory once it finds the \fIdebian/changelog\fR file, and check
that the directory name corresponds to the package name. Precisely
how it does this is controlled by two configuration file variables
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR and \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR, and
their corresponding command-line options \fB\-\-check-dirname-level\fR
and \fB\-\-check-dirname-regex\fR.
.PP
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR can take the following values:
.TP
.B 0
Never check the directory name.
.TP
.B 1
Only check the directory name if we have had to change directory in
our search for \fIdebian/changelog\fR. This is the default behaviour.
.TP
.B 2
Always check the directory name.
.PP
The directory name is checked by testing whether the current directory
name (as determined by \fBpwd\fR(1)) matches the regex given by the
configuration file option \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR or by the
command line option \fB\-\-check-dirname-regex\fR \fIregex\fR. Here
\fIregex\fR is a Perl regex (see \fBperlre\fR(3perl)), which will be
anchored at the beginning and the end. If \fIregex\fR contains a '/',
then it must match the full directory path. If not, then it must
match the full directory name. If \fIregex\fR contains the string
\'PACKAGE', this will be replaced by the source package name, as
determined from the changelog. The default value for the regex is:
\'PACKAGE(-.+)?', thus matching directory names such as PACKAGE and
PACKAGE-version.
.SH OPTIONS
.TP
\fB\-a\fIdebian-architecture\fR, \fB\-t\fIGNU-system-type\fR
See \fBdpkg-architecture\fR(1) for a description of these options.
They affect the search for the \fI.changes\fR file. They are provided
to mimic the behaviour of \fBdpkg-buildpackage\fR when determining the
name of the \fI.changes\fR file.
.TP
\fB\-\-debs\-dir\fR \fIdirectory\fR
Look for the \fI.changes\fR and \fI.deb\fR files in \fIdirectory\fR
instead of the parent of the source directory. This should
either be an absolute path or relative to the top of the source
directory.
.TP
.BR \-m ", " \-\-multi
Search for a multiarch \fI.changes\fR file, as created by \fBdpkg-cross\fR.
.TP
.BR \-u ", " \-\-upgrade
Only upgrade packages already installed on the system, rather than
installing all packages listed in the \fI.changes\fR file.
Useful for multi-binary packages when you don't want to have all the
binaries installed at once.
.TP
\fB\-\-check-dirname-level\fR \fIN\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-check-dirname-regex\fR \fIregex\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-with-depends\fR
Attempt to satisfy the \fIDepends\fR of a package when installing it.
.TP
\fB\-\-tool\fR \fItool\fR
Use the specified \fItool\fR for installing the dependencies of the package(s) to be
installed. By default, \fBapt-get\fR is used.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
\fB\-\-help\fR, \fB\-\-version\fR
Show help message and version information respectively.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variables are:
.TP
.B DEBRELEASE_DEBS_DIR
This specifies the directory in which to look for the \fI.changes\fR
and \fI.deb\fR files, and is either an absolute path or relative to
the top of the source tree. This corresponds to the
\fB\-\-debs\-dir\fR command line option. This directive could be
used, for example, if you always use \fBpbuilder\fR or
\fBsvn-buildpackage\fR to build your packages. Note that it also
affects \fBdebrelease\fR(1) in the same way, hence the strange name of
the option.
.TP
.BR DEVSCRIPTS_CHECK_DIRNAME_LEVEL ", " DEVSCRIPTS_CHECK_DIRNAME_REGEX
See the above section \fBDirectory name checking\fR for an explanation of
these variables. Note that these are package-wide configuration
variables, and will therefore affect all \fBdevscripts\fR scripts
which check their value, as described in their respective manpages and
in \fBdevscripts.conf\fR(5).
.SH "SEE ALSO"
.BR devscripts.conf (5)
.SH AUTHOR
\fBdebi\fR was originally written by Christoph Lameter
<clameter@debian.org>. The now-defunct script \fBdebit\fR was
originally written by James R. Van Zandt <jrv@vanzandt.mv.com>. They
have been moulded into one script together with \fBdebc\fR(1) and
parts extensively modified by Julian Gilbey <jdg@debian.org>.

View file

@ -0,0 +1,23 @@
# /usr/share/bash-completion/completions/debi
# Bash command completion for debi(1).
# Documentation: bash(1), section “Programmable Completion”.
_debc()
{
local cur
cur="${COMP_WORDS[COMP_CWORD]}"
COMPREPLY=($(compgen -f -X '!*.changes' -- "$cur"))
if echo "$cur" | grep -qs '^[a-z0-9+.-]*$'; then
COMPREPLY=(${COMPREPLY[@]} $(apt-cache pkgnames -- $cur 2> /dev/null))
fi
return 0
}
complete -o dirnames -F _debc debc debi
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

477
scripts/debi.pl Executable file
View file

@ -0,0 +1,477 @@
#!/usr/bin/perl
# debi: Install current version of deb package
# debc: List contents of current version of deb package
#
# debi and debc originally by Christoph Lameter <clameter@debian.org>
# Copyright Christoph Lameter <clameter@debian.org>
# The now defunct debit originally by Jim Van Zandt <jrv@vanzandt.mv.com>
# Copyright 1999 Jim Van Zandt <jrv@vanzandt.mv.com>
# Modifications by Julian Gilbey <jdg@debian.org>, 1999-2003
# Copyright 1999-2003, Julian Gilbey <jdg@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use 5.008;
use strict;
use warnings;
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use File::Basename;
use filetest 'access';
use Cwd;
use Dpkg::Control;
use Dpkg::Changelog::Parse qw(changelog_parse);
use Dpkg::IPC;
my $progname = basename($0, '.pl'); # the '.pl' is for when we're debugging
my $modified_conf_msg;
sub usage_i {
print <<"EOF";
Usage: $progname [options] [.changes file] [package ...]
Install the .deb file(s) just created, as listed in the generated
.changes file or the .changes file specified. If packages are listed,
only install those specified packages from the .changes file.
Options:
--no-conf or Don\'t read devscripts config files;
--noconf must be the first option given
-a<arch> Search for .changes file made for Debian build <arch>
-t<target> Search for .changes file made for GNU <target> arch
--debs-dir DIR Look for the changes and debs files in DIR instead of
the parent of the current package directory
--multi Search for multiarch .changes file made by dpkg-cross
--upgrade Only upgrade packages; don't install new ones.
--check-dirname-level N
How much to check directory names:
N=0 never
N=1 only if program changes directory (default)
N=2 always
--check-dirname-regex REGEX
What constitutes a matching directory name; REGEX is
a Perl regular expression; the string \`PACKAGE\' will
be replaced by the package name; see manpage for details
(default: 'PACKAGE(-.+)?')
--with-depends Install packages with their depends.
--tool TOOL Use the specified tool for installing the dependencies
of the package(s) to be installed.
(default: apt-get)
--help Show this message
--version Show version and copyright information
Default settings modified by devscripts configuration files:
$modified_conf_msg
EOF
}
sub usage_c {
print <<"EOF";
Usage: $progname [options] [.changes file] [package ...]
Display the contents of the .deb or .udeb file(s) just created, as listed
in the generated .changes file or the .changes file specified.
If packages are listed, only display those specified packages
from the .changes file. Options:
--no-conf or Don\'t read devscripts config files;
--noconf must be the first option given
-a<arch> Search for changes file made for Debian build <arch>
-t<target> Search for changes file made for GNU <target> arch
--debs-dir DIR Look for the changes and debs files in DIR instead of
the parent of the current package directory
--list-changes only list the .changes file
--list-debs only list the .deb files; don't display their contents
--multi Search for multiarch .changes file made by dpkg-cross
--check-dirname-level N
How much to check directory names:
N=0 never
N=1 only if program changes directory (default)
N=2 always
--check-dirname-regex REGEX
What constitutes a matching directory name; REGEX is
a Perl regular expression; the string \`PACKAGE\' will
be replaced by the package name; see manpage for details
(default: 'PACKAGE(-.+)?')
--help Show this message
--version Show version and copyright information
Default settings modified by devscripts configuration files:
$modified_conf_msg
EOF
}
if ($progname eq 'debi') { *usage = \&usage_i; }
elsif ($progname eq 'debc') { *usage = \&usage_c; }
else { die "Unrecognised invocation name: $progname\n"; }
my $version = <<"EOF";
This is $progname, from the Debian devscripts package, version ###VERSION###
This code is copyright 1999-2003, Julian Gilbey <jdg\@debian.org>,
all rights reserved.
Based on original code by Christoph Lameter and James R. Van Zandt.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of
the GNU General Public License, version 2 or later.
EOF
# Start by setting default values
my $debsdir;
my $debsdir_warning;
my $check_dirname_level = 1;
my $check_dirname_regex = 'PACKAGE(-.+)?';
my $install_tool = (-t STDOUT ? 'apt' : 'apt-get');
# Next, read configuration files and then command line
# The next stuff is boilerplate
if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
$modified_conf_msg = " (no configuration files read)";
shift;
} else {
my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
my %config_vars = (
'DEBRELEASE_DEBS_DIR' => '..',
'DEVSCRIPTS_CHECK_DIRNAME_LEVEL' => 1,
'DEVSCRIPTS_CHECK_DIRNAME_REGEX' => 'PACKAGE(-.+)?',
);
my %config_default = %config_vars;
my $shell_cmd;
# Set defaults
foreach my $var (keys %config_vars) {
$shell_cmd .= qq[$var="$config_vars{$var}";\n];
}
$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
# Read back values
foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
my $shell_out = `/bin/bash -c '$shell_cmd'`;
@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
# Check validity
$config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'} =~ /^[012]$/
or $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'} = 1;
# We do not replace this with a default directory to avoid accidentally
# installing a broken package
$config_vars{'DEBRELEASE_DEBS_DIR'} =~ s%/+%/%;
$config_vars{'DEBRELEASE_DEBS_DIR'} =~ s%(.)/$%$1%;
$debsdir_warning
= "config file specified DEBRELEASE_DEBS_DIR directory $config_vars{'DEBRELEASE_DEBS_DIR'} does not exist!";
foreach my $var (sort keys %config_vars) {
if ($config_vars{$var} ne $config_default{$var}) {
$modified_conf_msg .= " $var=$config_vars{$var}\n";
}
}
$modified_conf_msg ||= " (none)\n";
chomp $modified_conf_msg;
$debsdir = $config_vars{'DEBRELEASE_DEBS_DIR'};
$check_dirname_level = $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'};
$check_dirname_regex = $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_REGEX'};
}
# Command line options next
my ($opt_help, $opt_version, $opt_a, $opt_t, $opt_debsdir, $opt_multi);
my $opt_upgrade;
my ($opt_level, $opt_regex, $opt_noconf);
my ($opt_tool, $opt_with_depends);
my ($opt_list_changes, $opt_list_debs);
GetOptions(
"help" => \$opt_help,
"version" => \$opt_version,
"a=s" => \$opt_a,
"t=s" => \$opt_t,
"debs-dir=s" => \$opt_debsdir,
"m|multi" => \$opt_multi,
"u|upgrade" => \$opt_upgrade,
"check-dirname-level=s" => \$opt_level,
"check-dirname-regex=s" => \$opt_regex,
"with-depends" => \$opt_with_depends,
"tool=s" => \$opt_tool,
"noconf" => \$opt_noconf,
"no-conf" => \$opt_noconf,
"list-changes" => \$opt_list_changes,
"list-debs" => \$opt_list_debs,
)
or die
"Usage: $progname [options] [.changes file] [package ...]\nRun $progname --help for more details\n";
if ($opt_help) { usage(); exit 0; }
if ($opt_version) { print $version; exit 0; }
if ($opt_noconf) {
die
"$progname: --no-conf is only acceptable as the first command-line option!\n";
}
my ($targetarch, $targetgnusystem);
$targetarch = $opt_a ? "-a$opt_a" : "";
$targetgnusystem = $opt_t ? "-t$opt_t" : "";
if (defined $opt_level) {
if ($opt_level =~ /^[012]$/) { $check_dirname_level = $opt_level; }
else {
die
"$progname: unrecognised --check-dirname-level value (allowed are 0,1,2)\n";
}
}
if (defined $opt_regex) { $check_dirname_regex = $opt_regex; }
if ($opt_tool) {
$install_tool = $opt_tool;
}
# Is a .changes file listed on the command line?
my ($changes, $mchanges, $arch);
if (@ARGV and $ARGV[0] =~ /\.changes$/) {
$changes = shift;
}
# Need to determine $arch in any event
$arch = `dpkg-architecture $targetarch $targetgnusystem -qDEB_HOST_ARCH`;
if ($? != 0 or !$arch) {
die "$progname: unable to determine target architecture.\n";
}
chomp $arch;
my @foreign_architectures;
unless ($opt_a || $opt_t || $progname eq 'debc') {
@foreign_architectures
= map { chomp; $_ } `dpkg --print-foreign-architectures`;
}
my $chdir = 0;
if (!defined $changes) {
if ($opt_debsdir) {
$opt_debsdir =~ s%/+%/%;
$opt_debsdir =~ s%(.)/$%$1%;
$debsdir_warning = "--debs-dir directory $opt_debsdir does not exist!";
$debsdir = $opt_debsdir;
}
if (!-d $debsdir) {
die "$progname: $debsdir_warning\n";
}
# Look for .changes file via debian/changelog
until (-r 'debian/changelog') {
$chdir = 1;
chdir '..' or die "$progname: can't chdir ..: $!\n";
if (cwd() eq '/') {
die
"$progname: cannot find readable debian/changelog anywhere!\nAre you in the source code tree?\n";
}
}
if (-e ".svn/deb-layout") {
# Cope with format of svn-buildpackage tree
my $fh;
open($fh, "<", ".svn/deb-layout")
|| die "Can't open .svn/deb-layout: $!\n";
my ($build_area) = grep /^buildArea=/, <$fh>;
close($fh);
if (defined($build_area) and not $opt_debsdir) {
chomp($build_area);
$build_area =~ s/^buildArea=//;
$debsdir = $build_area if -d $build_area;
}
}
# Find the source package name and version number
my $changelog = changelog_parse();
die "$progname: no package name in changelog!\n"
unless exists $changelog->{'Source'};
die "$progname: no package version in changelog!\n"
unless exists $changelog->{'Version'};
# Is the directory name acceptable?
if ($check_dirname_level == 2
or ($check_dirname_level == 1 and $chdir)) {
my $re = $check_dirname_regex;
$re =~ s/PACKAGE/\\Q$changelog->{'Source'}\\E/g;
my $gooddir;
if ($re =~ m%/%) { $gooddir = eval "cwd() =~ /^$re\$/;"; }
else { $gooddir = eval "basename(cwd()) =~ /^$re\$/;"; }
if (!$gooddir) {
my $pwd = cwd();
die <<"EOF";
$progname: found debian/changelog for package $changelog->{'Source'} in the directory
$pwd
but this directory name does not match the package name according to the
regex $check_dirname_regex.
To run $progname on this package, see the --check-dirname-level and
--check-dirname-regex options; run $progname --help for more info.
EOF
}
}
my $sversion = $changelog->{'Version'};
$sversion =~ s/^\d+://;
my $package = $changelog->{'Source'};
my $pva = "${package}_${sversion}_${arch}";
$changes = "$debsdir/$pva.changes";
if (!-e $changes and -d "../build-area") {
# Try out default svn-buildpackage structure in case
# we were going to fail anyway...
$changes = "../build-area/$pva.changes";
}
if ($opt_multi) {
my @mchanges = glob("$debsdir/${package}_${sversion}_*+*.changes");
@mchanges = grep { /[_+]$arch[\.+]/ } @mchanges;
$mchanges = $mchanges[0] || '';
$mchanges ||= "$debsdir/${package}_${sversion}_multi.changes"
if -f "$debsdir/${package}_${sversion}_multi.changes";
}
}
if ($opt_list_changes) {
printf "%s\n", $changes;
exit(0);
}
chdir dirname($changes)
or die "$progname: can't chdir to $changes directory: $!\n";
$changes = basename($changes);
$mchanges = basename($mchanges) if $opt_multi;
if (!-r $changes or $opt_multi and $mchanges and !-r $mchanges) {
die "$progname: can't read $changes"
. (($opt_multi and $mchanges) ? " or $mchanges" : "") . "!\n";
}
if (!-r $changes and $opt_multi) {
$changes = $mchanges;
} else {
$opt_multi = 0;
}
# $opt_multi now tells us whether we're actually using a multi-arch .changes
# file
my @debs = ();
my %pkgs = map { $_ => 0 } @ARGV;
my $ctrl = Dpkg::Control->new(name => $changes, type => CTRL_FILE_CHANGES);
$ctrl->load($changes);
for (split(/\n/, $ctrl->{Files})) {
# udebs are only supported for debc
if ( (($progname eq 'debi') && (/ (\S*\.deb)$/))
|| (($progname eq 'debc') && (/ (\S*\.u?deb)$/))) {
my $deb = $1;
open(my $stdout, '-|', 'dpkg-deb', '-f', $deb);
my $fields = Dpkg::Control->new(name => $deb, type => CTRL_PKG_DEB);
$fields->parse($stdout, $deb);
my $pkg = $fields->{Package};
# don't want to install other archs' .debs, unless they are
# Multi-Arch: same:
next
unless (
$progname eq 'debc'
|| $fields->{Architecture} eq 'all'
|| $fields->{Architecture} eq $arch
|| (($fields->{'Multi-Arch'} || 'no') eq 'same'
&& grep { $_ eq $fields->{Architecture} }
@foreign_architectures));
if (@ARGV) {
if (exists $pkgs{$pkg}) {
push @debs, $deb;
$pkgs{$pkg}++;
} elsif (exists $pkgs{$deb}) {
push @debs, $deb;
$pkgs{$deb}++;
}
} else {
push @debs, $deb;
}
}
}
if (!@debs) {
die
"$progname: no appropriate .debs found in the changes file $changes!\n";
}
if ($progname eq 'debi') {
my @upgrade = $opt_upgrade ? ('-O') : ();
if ($opt_with_depends) {
if ($install_tool =~ /^apt(?:-get)?$/ && !$opt_upgrade) {
spawn(
exec =>
[$install_tool, 'install', '--reinstall', "./$changes"],
wait_child => 1
);
} else {
my @apt_opts;
if ($install_tool =~ /^apt(?:-get)?$/) {
push @apt_opts, '--with-source', "./$changes";
}
spawn(
exec => ['dpkg', @upgrade, '--unpack', @debs],
wait_child => 1
);
spawn(
exec => [$install_tool, @apt_opts, '-f', 'install'],
wait_child => 1
);
}
} else {
if ($install_tool =~ /^apt(?:-get)?$/ && $opt_upgrade) {
spawn(
exec => [
$install_tool, 'install',
'--only-upgrade', '--reinstall',
"./$changes"
],
wait_child => 1
);
} else {
spawn(exec => ['dpkg', @upgrade, '-i', @debs], wait_child => 1);
}
}
} else {
# $progname eq 'debc'
foreach my $deb (@debs) {
if ($opt_list_debs) {
printf "%s/%s\n", cwd(), $deb;
next;
}
print "$deb\n";
print '-' x length($deb), "\n";
system('dpkg-deb', '-I', $deb) == 0
or die "$progname: dpkg-deb -I $deb failed\n";
system('dpkg-deb', '-c', $deb) == 0
or die "$progname: dpkg-deb -c $deb failed\n";
print "\n";
}
}
# Now do a sanity check
if (@ARGV) {
foreach my $pkg (keys %pkgs) {
if ($pkgs{$pkg} == 0) {
warn "$progname: package $pkg not found in $changes, ignoring\n";
} elsif ($pkgs{$pkg} > 1) {
warn
"$progname: package $pkg found more than once in $changes, installing all\n";
}
}
}
exit 0;

639
scripts/debootsnap Executable file
View file

@ -0,0 +1,639 @@
#!/usr/bin/env python3
#
# Copyright 2021 Johannes Schauer Marin Rodrigues <josch@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# This tool is similar to debootstrap but is able to recreate a chroot tarball
# containing precisely the given package and version selection. The package
# list is expected on standard input and may be of the format produced by:
#
# dpkg-query --showformat '${binary:Package}=${Version}\n' --show
# The name was suggested by Adrian Bunk as a portmanteau of debootstrap and
# snapshot.debian.org.
# TODO: Address invalid names
# pylint: disable=invalid-name
import argparse
import atexit
import dataclasses
import difflib
import http.server
import os
import pathlib
import re
import shutil
import subprocess
import sys
import tempfile
import threading
from collections import defaultdict
from contextlib import contextmanager
from functools import partial
from operator import itemgetter
import requests
from debian.deb822 import BuildInfo
from devscripts.proxy import setupcache
class MyHTTPException(Exception):
pass
class MyHTTP404Exception(Exception):
pass
class MyHTTPTimeoutException(Exception):
pass
class RetryCountExceeded(Exception):
pass
@dataclasses.dataclass
class Source:
archive: str
timestamp: str
suite: str
components: list[str]
def deb_line(self, host: str = "snapshot.debian.org") -> str:
return (
f"deb [check-valid-until=no] http://{host}/archive/{self.archive}"
f"/{self.timestamp}/ {self.suite} {' '.join(self.components)}\n"
)
def parse_buildinfo(val):
with open(val, encoding="utf8") as f:
buildinfo = BuildInfo(f)
pkgs = []
for dep in buildinfo.relations["installed-build-depends"]:
assert len(dep) == 1
dep = dep[0]
assert dep["arch"] is None
assert dep["restrictions"] is None
assert len(dep["version"]) == 2
rel, version = dep["version"]
assert rel == "="
pkgs.append((dep["name"], dep["archqual"], version))
return pkgs, buildinfo.get("Build-Architecture")
def parse_pkgs(val):
if val == "-":
val = sys.stdin.read()
if val.startswith("./") or val.startswith("/"):
val = pathlib.Path(val)
if not val.exists():
print(f"{val} does not exist", file=sys.stderr)
sys.exit(1)
val = val.read_text(encoding="utf8")
pkgs = []
pattern = re.compile(
r"""
^[^a-z0-9]* # garbage at the beginning
([a-z0-9][a-z0-9+.-]+) # package name
(?:[^a-z0-9+.-]+([a-z0-9-]+))? # optional version
[^A-Za-z0-9.+~:-]+ # optional garbage
([A-Za-z0-9.+~:-]+) # version
[^A-Za-z0-9.+~:-]*$ # garbage at the end
""",
re.VERBOSE,
)
for line in re.split(r"[,\r\n]+", val):
if not line:
continue
match = pattern.fullmatch(line)
if match is None:
print(f"cannot parse: {line}", file=sys.stderr)
sys.exit(1)
pkgs.append(match.groups())
return [pkgs]
def get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
Combines debootstrap and snapshot.debian.org to create a chroot tarball with
exact package versions from the past either to reproduce bugs or to test source
package reproducibility.
To obtain a list of packages run the following command on one machine:
$ dpkg-query --showformat '${binary:Package}=${Version}\\n' --show
And pass the output to debootsnap with the --packages argument. The result
will be a chroot tarball with precisely the package versions as they were
found on the system that ran dpkg-query.
""",
epilog="""\
*EXAMPLES*
On one system run:
$ dpkg-query --showformat '${binary:Package}=${Version}\\n' --show > pkglist
Then copy over "pkglist" and on another system run:
$ debootsnap --pkgs=./pkglist > ./chroot.tar
Or use a buildinfo file as input:
$ debootsnap --buildinfo=./package.buildinfo > ./chroot.tar
A tarball of a chroot with precisely the requested package versions then be
found in the file `./chroot.tar`.
""",
)
parser.add_argument(
"--architecture",
"--nativearch",
help="native architecture of the chroot. Ignored if --buildinfo is"
" used. Foreign architectures are inferred from the package list."
" Not required if packages are architecture qualified.",
)
parser.add_argument(
"--ignore-notfound",
action="store_true",
help="only warn about packages that cannot be found on "
"snapshot.debian.org instead of exiting",
)
parser.add_argument(
"--cache", help="cache directory -- by default $TMPDIR is used", type=str
)
parser.add_argument(
"--port",
help="manually choose port number for the apt cache instead of "
"automatically choosing a free port",
type=int,
default=0,
)
parser.add_argument("--nocache", help="disable cache", action="store_true")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--buildinfo",
type=parse_buildinfo,
help="use packages from a buildinfo file. Read buildinfo file from "
'standard input if value is "-".',
)
group.add_argument(
"--packages",
"--pkgs",
action="extend",
type=parse_pkgs,
help="list of packages, optional architecture and version, separated "
"by comma or linebreak. Read list from standard input if value is "
'"-". Read list from a file if value starts with "./" or "/". The '
"option can be specified multiple times. Package name, "
"version and architecture are separated by one or more characters "
"that are not legal in the respective adjacent field. Leading and "
"trailing illegal characters are allowed. Example: "
"pkg1:arch=ver1,pkg2:arch=ver2",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--sources-list-only",
action="store_true",
help="only query metasnap.debian.net and print the sources.list "
"needed to create chroot and exit",
)
group.add_argument(
"output",
nargs="?",
default="-",
help="path to output chroot tarball",
)
return parser
def query_metasnap(pkgsleft, archive, nativearch):
handled_pkgs = set(pkgsleft)
r = requests.post(
"http://metasnap.debian.net/cgi-bin/api",
files={
"archive": archive,
"arch": nativearch,
"pkgs": ",".join([n + ":" + a + "=" + v for n, a, v in handled_pkgs]),
},
timeout=60,
)
if r.status_code == 404:
for line in r.text.splitlines():
n, a, v = line.split()
handled_pkgs.remove((n, a, v))
r = requests.post(
"http://metasnap.debian.net/cgi-bin/api",
files={
"archive": archive,
"arch": nativearch,
"pkgs": ",".join([n + ":" + a + "=" + v for n, a, v in handled_pkgs]),
},
timeout=60,
)
assert r.status_code == 200, r.text
suite2pkgs = defaultdict(set)
pkg2range = {}
for line in r.text.splitlines():
n, a, v, s, c, b, e = line.split()
assert (n, a, v) in handled_pkgs
suite2pkgs[s].add((n, a, v))
# this will only keep one range of packages with multiple
# ranges but we don't care because we only need one
pkg2range[((n, a, v), s)] = (c, b, e)
return handled_pkgs, suite2pkgs, pkg2range
def comp_ts(ranges):
last = "19700101T000000Z" # impossibly early date
res = []
for c, b, e in ranges:
if last >= b:
# add the component the current timestamp needs
res[-1][1].add(c)
continue
# add new timestamp with initial component
last = e
res.append((last, set([c])))
return res
def compute_sources(pkgs, nativearch, ignore_notfound) -> list[Source]:
sources = []
pkgsleft = set(pkgs)
for archive in [
"debian",
"debian-debug",
"debian-security",
"debian-ports",
"debian-volatile",
"debian-backports",
]:
if len(pkgsleft) == 0:
break
handled_pkgs, suite2pkgs, pkg2range = query_metasnap(
pkgsleft, archive, nativearch
)
# greedy algorithm:
# pick the suite covering most packages first
while len(handled_pkgs) > 0:
bestsuite = sorted(suite2pkgs.items(), key=lambda v: len(v[1]))[-1][0]
ranges = [pkg2range[nav, bestsuite] for nav in suite2pkgs[bestsuite]]
# sort by end-time
ranges.sort(key=itemgetter(2))
for ts, comps in comp_ts(ranges):
sources.append(Source(archive, ts, bestsuite, comps))
for nav in suite2pkgs[bestsuite]:
handled_pkgs.remove(nav)
pkgsleft.remove(nav)
for suite in suite2pkgs:
if suite == bestsuite:
continue
if nav in suite2pkgs[suite]:
suite2pkgs[suite].remove(nav)
del suite2pkgs[bestsuite]
if pkgsleft:
print("cannot find:", file=sys.stderr)
print(
"\n".join([f"{pkg[0]}:{pkg[1]}={pkg[2]}" for pkg in pkgsleft]),
file=sys.stderr,
)
if not ignore_notfound:
sys.exit(1)
return sources
def create_install_hook(tmpdirname, deb_files):
# manually feed apt install the complete package list
# to be sure that the apt solver did not change it.
hook = pathlib.Path(tmpdirname) / "apt_install.sh"
hook.write_text(
"\n".join(
[
"#!/bin/sh",
"cat << END | APT_CONFIG=$MMDEBSTRAP_APT_CONFIG"
" xargs apt-get install --yes",
"\n".join(deb_files),
"END",
]
)
)
hook.chmod(0o755)
def create_repo(tmpdirname, pkgs):
with open(tmpdirname + "/control", "w", encoding="utf8") as f:
def pkg2name(n, a, v):
if a is None:
return f"{n} (= {v})"
return f"{n}:{a} (= {v})"
f.write("Package: debootsnap-dummy\n")
f.write(f"Depends: {', '.join([pkg2name(*pkg) for pkg in pkgs])}\n")
subprocess.check_call(
["equivs-build", tmpdirname + "/control"],
cwd=tmpdirname + "/cache",
# equivs-build behaves differently depending on whether TMPDIR is set
# or not, so to force the same behaviour independent on whether the
# user has TMPDIR set, we set or override that variable with our own
# (which is a path below the user's $TMPDIR anyways, if it was set)
env={**os.environ, "TMPDIR": tmpdirname + "/cache"},
)
packages_content = subprocess.check_output(
["apt-ftparchive", "packages", "."], cwd=tmpdirname + "/cache"
)
with open(tmpdirname + "/cache/Packages", "wb") as f:
f.write(packages_content)
release_content = subprocess.check_output(
[
"apt-ftparchive",
"release",
"-oAPT::FTPArchive::Release::Suite=dummysuite",
".",
],
cwd=tmpdirname + "/cache",
)
with open(tmpdirname + "/cache/Release", "wb") as f:
f.write(release_content)
@contextmanager
def serve_repo(tmpdirname):
httpd = http.server.HTTPServer(
("localhost", 0),
partial(http.server.SimpleHTTPRequestHandler, directory=tmpdirname + "/cache"),
)
# run server in a new thread
server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.daemon = True
# start thread
server_thread.start()
# retrieve port (in case it was generated automatically)
_, port = httpd.server_address
try:
yield port
finally:
httpd.shutdown()
httpd.server_close()
server_thread.join()
def run_mmdebstrap(
tmpdirname, sources: list[Source], nativearch, foreignarches, output
):
with open(tmpdirname + "/sources.list", "w", encoding="utf8") as f:
for source in sources:
f.write(source.deb_line())
# we serve the directory via http instead of using a copy:// mirror
# because the temporary directory is not accessible to the unshared
# user
with serve_repo(tmpdirname) as port:
cmd = [
"mmdebstrap",
f"--architectures={','.join([nativearch] + list(foreignarches))}",
"--variant=essential",
"--include=debootsnap-dummy",
"--format=tar",
"--skip=cleanup/reproducible",
'--aptopt=Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"',
"--hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr",
f"--customize={tmpdirname}/apt_install.sh",
'--customize-hook=chroot "$1" dpkg -r debootsnap-dummy',
'--customize-hook=chroot "$1" dpkg-query --showformat '
"'${binary:Package}=${Version}\\n' --show > \"$1/pkglist\"",
"--customize-hook=download /pkglist ./pkglist",
'--customize-hook=rm "$1/pkglist"',
"--customize-hook=upload sources.list /etc/apt/sources.list",
"dummysuite",
output,
f"deb [trusted=yes] http://localhost:{port}/ ./",
]
subprocess.check_call(cmd, cwd=tmpdirname)
newpkgs = set()
with open(tmpdirname + "/pkglist", encoding="utf8") as f:
for line in f:
line = line.rstrip()
n, v = line.split("=")
a = nativearch
if ":" in n:
n, a = n.split(":")
newpkgs.add((n, a, v))
return newpkgs
# pylint: disable=too-many-locals
def download_packages(
tmpdirname,
sources: list[Source],
pkgs,
nativearch,
foreignarches,
cache,
nocache,
port,
):
for d in [
"/etc/apt/apt.conf.d",
"/etc/apt/sources.list.d",
"/etc/apt/preferences.d",
"/var/cache/apt",
"/var/lib/apt/lists/partial",
"/var/lib/dpkg",
]:
os.makedirs(tmpdirname + "/" + d)
# apt-get update requires /var/lib/dpkg/status
with open(tmpdirname + "/var/lib/dpkg/status", "w", encoding="utf8") as f:
pass
with open(tmpdirname + "/apt.conf", "w", encoding="utf8") as f:
f.write(f'Apt::Architecture "{nativearch}";\n')
f.write("Apt::Architectures { " + f'"{nativearch}"; ')
for a in foreignarches:
f.write(f'"{a}"; ')
f.write("};\n")
f.write('Dir "' + tmpdirname + '";\n')
f.write('Dir::Etc::Trusted "/etc/apt/trusted.gpg";\n')
f.write('Dir::Etc::TrustedParts "/usr/share/keyrings/";\n')
f.write('Acquire::Languages "none";\n')
# f.write("Acquire::http::Dl-Limit \"1000\";\n")
# f.write("Acquire::https::Dl-Limit \"1000\";\n")
f.write('Acquire::Retries "5";\n')
# ignore expired signatures
f.write('Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig";\n')
os.makedirs(tmpdirname + "/cache")
apt_env = {"APT_CONFIG": tmpdirname + "/apt.conf"}
if not nocache:
port, teardown = setupcache(cache, port)
apt_env["http_proxy"] = f"http://127.0.0.1:{port}"
atexit.register(teardown)
with open(tmpdirname + "/etc/apt/sources.list", "w", encoding="utf8") as f:
for source in sources:
f.write(source.deb_line("snapshot.debian.org"))
subprocess.check_call(["apt-get", "update", "--error-on=any"], env=apt_env)
deb_files = []
cache = pathlib.Path(tmpdirname, "cache")
for i, (name, arch, version) in enumerate(pkgs):
pkg = f"{name}:{arch}={version}"
print(f"Downloading dependency {i + 1} of {len(pkgs)}: {pkg}")
with tempfile.TemporaryDirectory() as tmpdir2:
subprocess.check_call(
["apt-get", "download", "--yes", pkg],
cwd=tmpdir2,
env=apt_env,
)
debs = os.listdir(tmpdir2)
assert len(debs) == 1
# Normalize the package name to how it appears in the archive.
# Mainly this removes the epoch from the filename, see
# https://bugs.debian.org/645895
# This avoids apt bugs connected with a percent sign in the
# filename as they occasionally appear, for example as
# introduced in apt 2.1.15 and later fixed by DonKult:
# https://salsa.debian.org/apt-team/apt/-/merge_requests/175
subprocess.check_call(["dpkg-name", tmpdir2 + "/" + debs[0]])
debs = os.listdir(tmpdir2)
assert len(debs) == 1
shutil.move(tmpdir2 + "/" + debs[0], cache)
deb_files.append((cache / debs[0]).as_posix())
return deb_files
def handle_packages(architecture, packages):
pkgs = [v for sublist in packages for v in sublist]
if architecture is None:
arches = {a for _, a, _ in pkgs if a is not None}
if len(arches) == 0:
print("packages are not architecture qualified", file=sys.stderr)
print("use --architecture to set the native architecture", file=sys.stderr)
sys.exit(1)
elif len(arches) > 1:
print("more than one architecture in the package list", file=sys.stderr)
print("use --architecture to set the native architecture", file=sys.stderr)
sys.exit(1)
nativearch = arches.pop()
assert arches == set()
else:
nativearch = architecture
return pkgs, nativearch
def main(arguments: list[str]) -> None:
parser = get_parser()
args = parser.parse_args(arguments)
if not args.sources_list_only and args.output == "-" and sys.stdout.isatty():
parser.print_usage()
print(
"E: Refusing to write tarball to interactive tty. "
"Redirect stdout to a file or pass the output tarball filename "
"as the positional argument [output]."
)
sys.exit(1)
if args.packages:
pkgs, nativearch = handle_packages(args.architecture, args.packages)
else:
pkgs, nativearch = args.buildinfo
# unknown architectures are the native architecture
pkgs = [(n, a if a is not None else nativearch, v) for n, a, v in pkgs]
# make package list unique
pkgs = list(set(pkgs))
# compute foreign architectures
foreignarches = set()
for _, a, _ in pkgs:
if a != nativearch:
foreignarches.add(a)
for tool in [
"equivs-build",
"apt-ftparchive",
"mmdebstrap",
"apt-get",
"dpkg-name",
]:
if shutil.which(tool) is None:
print(f"{tool} is required but not installed", file=sys.stderr)
sys.exit(1)
sources = compute_sources(pkgs, nativearch, args.ignore_notfound)
if args.sources_list_only:
for source in sources:
print(source.deb_line(), end="")
sys.exit(0)
with tempfile.TemporaryDirectory() as tmpdirname:
# chmod so mmdebstrap can run the generated install hook
os.chmod(tmpdirname, 0o711)
deb_files = download_packages(
tmpdirname,
sources,
pkgs,
nativearch,
foreignarches,
args.cache,
args.nocache,
args.port,
)
create_install_hook(tmpdirname, deb_files)
create_repo(tmpdirname, pkgs)
newpkgs = run_mmdebstrap(
tmpdirname, sources, nativearch, foreignarches, args.output
)
# make sure that the installed packages match the requested package
# list
if set(newpkgs) != set(pkgs):
diff = "\n".join(
difflib.unified_diff(
["_".join(pkg) for pkg in sorted(pkgs)],
["_".join(pkg) for pkg in sorted(newpkgs)],
fromfile="buildinfo",
tofile="bootstrapped",
lineterm="",
)
)
raise AssertionError(
"environment bootstrapped from buildinfo file does not match "
"environment in buildinfo file:\n\n" + diff
)
if __name__ == "__main__":
main(sys.argv[1:])

1
scripts/debootsnap.py Symbolic link
View file

@ -0,0 +1 @@
debootsnap

832
scripts/debrebuild.pl Executable file
View file

@ -0,0 +1,832 @@
#!/usr/bin/perl
#
# Copyright © 2014-2024 Johannes Schauer Marin Rodrigues <josch@debian.org>
# Copyright © 2020 Niels Thykier <niels@thykier.net>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
use strict;
use warnings;
use autodie;
use Getopt::Long qw(:config gnu_getopt no_bundling no_auto_abbrev);
use Dpkg::Control;
use Dpkg::Control::FieldsCore;
use Dpkg::Index;
use Dpkg::Deps;
use Dpkg::Source::Package;
use Dpkg::Version;
use File::Temp qw(tempfile tempdir);
use File::Path qw(make_path);
use File::HomeDir;
use JSON::PP;
use Time::Piece;
use File::Basename;
use File::Spec;
use List::Util qw(any none);
use English;
my $progname;
BEGIN {
$progname = basename($0);
eval { require String::ShellQuote; };
if ($@) {
if ($@ =~ /^Can\'t locate String\/ShellQuote\.pm/) {
die
"$progname: you must have the libstring-shellquote-perl package installed\n"
. "to use this script";
} else {
die
"$progname: problem loading the String::ShellQuote module:\n $@\n"
. "Have you installed the libstring-shellquote-perl package?";
}
}
eval {
require LWP::Simple;
require LWP::UserAgent;
require URI::Escape; # libwww-perl depends on liburi-perl
no warnings;
$LWP::Simple::ua
= LWP::UserAgent->new(agent => 'LWP::UserAgent/debrebuild');
$LWP::Simple::ua->env_proxy();
};
if ($@) {
if ($@ =~ m/Can\'t locate LWP/) {
die "$progname: you must have the libwww-perl package installed\n"
. "to use this script";
} else {
die "$progname: problem loading the LWP and URI modules:\n $@\n"
. "Have you installed the libwww-perl package?";
}
}
}
# Make sure that each print statement flushes standard output.
# This avoid having to manually flush when printing strings that do not end
# in a newline.
STDOUT->autoflush(1);
my $respect_build_path = 1;
my $use_tor = 0;
my $outdir = './';
my $builder = 'none';
my $cache;
my %OPTIONS = (
'help|h' => sub { usage(0); },
'use-tor-proxy!' => \$use_tor,
'respect-build-path!' => \$respect_build_path,
'buildresult=s' => \$outdir,
'builder=s' => \$builder,
'cache=s' => \$cache,
);
sub usage {
my ($exit_code) = @_;
$exit_code //= 0;
print <<EOF;
Usage: $progname [options] <buildinfo>
$progname <--help|-h>
Given a buildinfo file, builds the referenced source package in an environment
documented in the provided buildinfo file. The build can be performed by
sbuild or other builders in a chroot environment created by debootsnap. The
generated artifacts will be verified against the hashes from the buildinfo
file.
Options:
--help, -h Show this help and exit
--[no-]use-tor-proxy Whether to fetch resources via tor (socks://127.0.0.1:9050)
Assumes "apt-transport-tor" is installed both in host + chroot
--[no-]respect-build-path Whether to setup the build to use the Build-Path from the
provided .buildinfo file.
--buildresult Directory for the build artifacts (default: ./)
--builder=BUILDER Which building software should be used. Possible values are
none, sbuild, mmdebstrap, dpkg and sbuild+unshare. The default
is none. See section BUILDER for details.
Note: $progname can parse buildinfo files with and without a GPG signature. However,
the signature (if present) is discarded as debrebuild does not support verifying
it. If the authenticity or integrity of the buildinfo files are important to
you, checking these need to be done before invoking $progname, for example by using
dscverify.
EXAMPLES
\$ $progname --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfo
BUILDERS
debrebuild can use different backends to perform the actual package rebuild.
The desired backend is chosen using the --builder option. The default is
"none".
none Dry-run mode. No build is performed.
sbuild Use sbuild to build the package. This requires sbuild to be
setup with schroot chroots of Debian stable distributions.
mmdebstrap Use mmdebstrap to build the package. This requires no
setup and no superuser privileges.
dpkg Directly run apt-get and dpkg-buildpackage on the current
system without chroot. This requires root privileges.
sbuild+unshare Use sbuild with the unshare backend. This will create the
chroot and perform the build without superuser privileges
and without any setup.
UNSHARE
Before kernel 5.10.1 or before Debian 11 (Bullseye), unprivileged user
namespaces were disabled in Debian for security reasons. Refer to Debian bug
#898446 for details. To enable user namespaces, run:
\$ sudo sysctl -w kernel.unprivileged_userns_clone=1
The sbuild+unshare builder requires and the mmdebstrap builder benefits from
having unprivileged user namespaces activated. On Ubuntu they are enabled by
default.
LIMITATIONS
Currently, the code assumes that all packages were at some point part of Debian
unstable main. This fails for packages from Debian ports, packages from
experimental as well as for locally built packages or packages from third
party repositories. Enabling support for Debian ports and experimental is
conceptually possible and only needs somebody implementing it.
EOF
exit($exit_code);
}
GetOptions(%OPTIONS) or usage(1);
my $buildinfo = shift @ARGV;
if (not defined($buildinfo)) {
print STDERR "ERROR: Missing mandatory buildinfo filename\n";
print STDERR "\n";
usage(1);
}
if ($buildinfo eq '--help' or $buildinfo eq '-h') {
usage(0);
}
if ($buildinfo =~ m/^-/) {
print STDERR "ERROR: Unsupported option $buildinfo\n";
print STDERR "\n";
usage(1);
}
if (@ARGV) {
print STDERR "ERROR: This program requires exactly argument!\n";
print STDERR "\n";
usage(1);
}
my $base_mirror = "http://snapshot.debian.org/archive/debian";
if ($use_tor) {
$base_mirror = "tor+http://snapshot.debian.org/archive/debian";
eval {
$LWP::Simple::ua->proxy([qw(http https)] => 'socks://127.0.0.1:9050');
};
if ($@) {
if ($@ =~ m/Can\'t locate LWP/) {
die
"Unable to use tor: the liblwp-protocol-socks-perl package is not installed\n";
} else {
die "Unable to use tor: Couldn't load socks proxy support: $@\n";
}
}
}
# buildinfo support in libdpkg-perl (>= 1.18.11)
my $cdata = Dpkg::Control->new(type => CTRL_FILE_BUILDINFO, allow_pgp => 1);
if (not $cdata->load($buildinfo)) {
die "cannot load $buildinfo\n";
}
if ($cdata->get_option('is_pgp_signed')) {
print(
"$buildinfo contains a GPG signature which has NOT been validated\n");
} else {
print "$buildinfo was unsigned\n";
}
my @architectures = split /\s+/, $cdata->{"Architecture"};
my $build_source = (scalar(grep /^source$/, @architectures)) == 1;
my $build_archall = (scalar(grep /^all$/, @architectures)) == 1;
@architectures = grep { !/^source$/ && !/^all$/ } @architectures;
if (scalar @architectures > 1) {
die "more than one architecture in Architecture field\n";
}
my $build_archany = (scalar @architectures) == 1;
my $build_arch = $cdata->{"Build-Architecture"};
if (not defined($build_arch)) {
die "need Build-Architecture field\n";
}
my $host_arch = $cdata->{"Host-Architecture"};
if (not defined($host_arch)) {
$host_arch = $build_arch;
}
my $srcpkgname = $cdata->{Source};
my $srcpkgver = $cdata->{Version};
# in some cases the source field contains a version in the form: name (version)
# for example: binclock (1.5-6)
if ($srcpkgname =~ / /) {
# make $@ local, so we don't print "Undefined subroutine" error message
# in other parts where we evaluate $@
local $@ = '';
# field_parse_binary_source is only available starting with dpkg 1.21.0
eval { ($srcpkgname, $srcpkgver) = field_parse_binary_source($cdata); };
if ($@) {
($srcpkgname, $srcpkgver) = split / /, $srcpkgname, 2;
# Add a simple control check to avoid the worst surprises and stop
# obvious cases of garbage-in-garbage-out.
die("Unexpected source package name: ${srcpkgname}\n")
if $srcpkgname =~ m{[ \t_/\(\)<>!\n%&\$\#\@]};
# remove the surrounding parenthesis from the version
$srcpkgver =~ s/^\((.*)\)$/$1/;
}
}
if (!defined $srcpkgname) {
die "unable to obtain source package name from buildinfo\n";
}
if (!defined $srcpkgver) {
die "unable to obtain source package version from buildinfo\n";
}
my $srcpkgbinver
= $cdata->{Version}; # this version will include the binmu suffix
$srcpkgbinver =~ s/^\d+://;
my $new_buildinfo;
{
my $arch;
if ($build_archany) {
$arch = $host_arch;
} elsif ($build_archall) {
$arch = 'all';
} else {
die "nothing to build\n";
}
$new_buildinfo = "$outdir/${srcpkgname}_${srcpkgbinver}_$arch.buildinfo";
}
if (-e $new_buildinfo) {
my ($dev1, $ino1) = (lstat $buildinfo)[0, 1]
or die "cannot lstat $buildinfo: $!\n";
my ($dev2, $ino2) = (lstat $new_buildinfo)[0, 1]
or die "cannot lstat $new_buildinfo: $!\n";
if ($dev1 == $dev2 && $ino1 == $ino2) {
die( "E: refusing to overwrite the input buildinfo file\n"
. "E: Either pass an output directory via --buildresult "
. "or call debrebuild from a directory that does not include $buildinfo"
);
}
}
my $inst_build_deps = $cdata->{"Installed-Build-Depends"};
if (not defined($inst_build_deps)) {
die "need Installed-Build-Depends field\n";
}
my $custom_build_path = $respect_build_path ? $cdata->{'Build-Path'} : undef;
if (defined($custom_build_path)) {
if ($custom_build_path =~ m{['`\$\\"\(\)<>#]|(?:\a|/)[.][.](?:\z|/)}) {
warn(
"Retry build with --no-respect-build-path to ignore the Build-Path field.\n"
);
die(
"Refusing to use $custom_build_path as Build-Path: Looks too special to be true"
);
}
if ($custom_build_path eq '' or $custom_build_path !~ m{^/}) {
warn(
"Retry build with --no-respect-build-path to ignore the Build-Path field.\n"
);
die(
qq{Build-Path must be a non-empty absolute path (i.e. start with "/").\n}
);
}
print "Using defined Build-Path: ${custom_build_path}\n";
} else {
if ($respect_build_path) {
print
"No Build-Path defined; not setting a defined build path for this build.\n";
}
}
my $srcpkg = Dpkg::Source::Package->new();
$srcpkg->{fields}{'Source'} = $srcpkgname;
$srcpkg->{fields}{'Version'} = $srcpkgver;
my $dsc_fname
= (dirname($buildinfo)) . '/' . $srcpkg->get_basename(1) . ".dsc";
my $debsnapexe = 'debsnap';
if ($PROGRAM_NAME eq "scripts/debrebuild.pl" && -x "scripts/debsnap.pl") {
$debsnapexe = "scripts/debsnap.pl";
}
if (!-e $dsc_fname) {
print( "I: obtaining dsc using: $debsnapexe --force"
. " --destdir . $srcpkgname $srcpkgver\n");
0 == system $debsnapexe, '--force', '--verbose', '--destdir',
dirname($buildinfo), $srcpkgname, $srcpkgver
or die "$debsnapexe failed\n";
}
if (!-e $dsc_fname) {
die( "$debsnapexe failed to download "
. $srcpkg->get_basename(1)
. ".dsc\n");
}
print "I: verifying dsc...";
my $buildinfo_checksums = Dpkg::Checksums->new();
$buildinfo_checksums->add_from_control($cdata);
$buildinfo_checksums->add_from_file($dsc_fname,
key => $srcpkg->get_basename(1) . ".dsc");
print " successful!\n";
my $environment = $cdata->{"Environment"};
if (not defined($environment)) {
die "need Environment field\n";
}
$environment =~ s/\n/ /g; # remove newlines
$environment =~ s/^ //; # remove leading whitespace
my @environment;
foreach my $line (split /\n/, $cdata->{"Environment"}) {
chomp $line;
if ($line eq '') {
next;
}
my ($name, $val) = split /=/, $line, 2;
$val =~ s/^"(.*)"$/$1/;
push @environment, "$name=$val";
}
# gather all installed build-depends and figure out the version of base-files
my $base_files_version;
my $dpkg_version;
my @inst_build_deps = ();
$inst_build_deps
= deps_parse($inst_build_deps, reduce_arch => 0, build_dep => 0);
if (!defined $inst_build_deps) {
die "deps_parse failed\n";
}
foreach my $pkg ($inst_build_deps->get_deps()) {
if (!$pkg->isa('Dpkg::Deps::Simple')) {
die "dependency disjunctions are not allowed\n";
}
if (not defined($pkg->{package})) {
die "name undefined\n";
}
if (defined($pkg->{relation})) {
if ($pkg->{relation} ne "=") {
die "wrong relation";
}
if (not defined($pkg->{version})) {
die "version undefined\n";
}
} else {
die "no version";
}
if ($pkg->{package} eq "base-files") {
if (defined($base_files_version)) {
die "more than one base-files\n";
}
$base_files_version = $pkg->{version};
} elsif ($pkg->{package} eq "dpkg") {
if (defined($dpkg_version)) {
die "more than one dpkg\n";
}
$dpkg_version = $pkg->{version};
}
push @inst_build_deps,
{
name => $pkg->{package},
architecture => $pkg->{archqual},
version => $pkg->{version} };
}
if (!defined($base_files_version)) {
die "no base-files\n";
}
if ($builder ne "none") {
if (!-e $outdir) {
make_path($outdir);
}
}
my $build = '';
my $changesarch = '';
if ($build_archany and $build_archall) {
$build = "binary";
$changesarch = $host_arch;
} elsif ($build_archany and !$build_archall) {
$build = "any";
$changesarch = $host_arch;
} elsif (!$build_archany and $build_archall) {
$build = "all";
$changesarch = 'all';
} else {
die "nothing to build\n";
}
my @install = ();
foreach my $pkg (@inst_build_deps) {
my $pkg_name = $pkg->{name};
my $pkg_ver = $pkg->{version};
my $pkg_arch = $pkg->{architecture};
if ( not defined $pkg_arch
or $pkg_arch eq "all"
or $pkg_arch eq $build_arch) {
push @install, "$pkg_name=$pkg_ver";
} else {
push @install, "$pkg_name:$pkg_arch=$pkg_ver";
}
}
my $debootsnapexe = 'debootsnap';
if ($PROGRAM_NAME eq "scripts/debrebuild.pl" && -x "scripts/debootsnap.py") {
$debootsnapexe = "scripts/debootsnap.py";
}
# File::Temp has an END block which cleans up the temporary directory
# we created with CLEANUP=>1 but we have to explicitly die() or otherwise
# the interpreter will exit on HUP, INT, PIPE and TERM instead of calling
# the END block
use sigtrap qw(die normal-signals);
# with CLEANUP=>1 this directory will automatically be removed once the
# program exits
my $tmpdir = tempdir('debrebuildXXXXXX', TMPDIR => 1, CLEANUP => 1);
my $tarballpath = '';
my $sourceslist = '';
if (any { $_ eq $builder } ('none', 'dpkg')) {
open my $fh, '-|', $debootsnapexe, "--buildinfo=$buildinfo",
'--sources-list-only' // die "cannot exec $debootsnapexe";
$sourceslist = do { local $/; <$fh> };
close $fh;
} elsif (any { $_ eq $builder } ('mmdebstrap', 'sbuild', 'sbuild+unshare')) {
(undef, $tarballpath)
= tempfile('debrebuild.tar.XXXXXXXXXXXX', OPEN => 0, DIR => $tmpdir);
0 == system $debootsnapexe, ($cache ? "--cache=$cache" : ()),
"--buildinfo=$buildinfo", $tarballpath
or die "$debootsnapexe failed";
} else {
die "unsupported builder: $builder\n";
}
if ($builder eq "none") {
print "\n";
print "Manual installation and build\n";
print "-----------------------------\n";
print "\n";
print
"The following sources.list contains all the required repositories:\n";
print "\n";
print "$sourceslist\n";
print "\n";
print "You can manually install the right dependencies like this:\n";
print "\n";
print "apt-get install --no-install-recommends";
# Release files from snapshots.d.o have often expired by the time
# we fetch them. Include the option to work around that to assist
# the user.
print " -oAcquire::Check-Valid-Until=false";
foreach my $pkg (@install) {
print " $pkg";
}
print "\n";
print "\n";
print "And then build your package:\n";
print "\n";
if ($custom_build_path) {
require Cwd;
my $custom_build_parent_dir = dirname($custom_build_path);
my $dsc_path = Cwd::realpath($dsc_fname)
// die("Cannot resolve ${dsc_fname}: $!\n");
print "mkdir -p \"${custom_build_parent_dir}\"\n";
print qq{dpkg-source -x "${dsc_path}" "${custom_build_path}"\n};
print "cd \"$custom_build_path\"\n";
} else {
print qq{dpkg-source -x "${dsc_fname}"\n};
print "cd packagedirectory\n";
}
print "\n";
if ($cdata->{"Binary-Only-Changes"}) {
print( "Since this is a binNMU, you must put the following "
. "lines at the top of debian/changelog:\n\n");
print($cdata->{"Binary-Only-Changes"});
}
print "\n";
print( "$environment dpkg-buildpackage -uc "
. "--host-arch=$host_arch --build=$build\n");
} elsif ($builder eq "dpkg") {
if ("$build_arch\n" ne `dpkg --print-architecture`) {
die "must be run on $build_arch\n";
}
if ($> != 0) {
die "you must be root for the dpkg builder\n";
}
if (-e $custom_build_path) {
die "$custom_build_path exists -- refusing to overwrite\n";
}
my $sources = '/etc/apt/sources.list.d/debrebuild.list';
if (-e $sources) {
die "$sources already exists -- refusing to overwrite\n";
}
open(FH, '>', $sources) or die "cannot open $sources: $!\n";
print FH "$sourceslist\n";
close FH;
my $config = '/etc/apt/apt.conf.d/23-debrebuild.conf';
if (-e $config) {
die "$config already exists -- refusing to overwrite\n";
}
open(FH, '>', $config) or die "cannot open $config: $!\n";
my @common_aptopts = (
'Acquire::Check-Valid-Until "false";',
'Acquire::http::Dl-Limit "1000";',
'Acquire::https::Dl-Limit "1000";',
'Acquire::Retries "5";',
'APT::Get::allow-downgrades "true";',
);
foreach my $line (@common_aptopts) {
print FH "$line\n";
}
close FH;
0 == system 'apt-get', 'update' or die "apt-get update failed\n";
my @cmd
= ('apt-get', 'install', '--no-install-recommends', '--yes', @install);
0 == system @cmd or die "apt-get install failed\n";
0 == system 'apt-get', 'source', '--only-source', '--download-only',
"$srcpkgname=$srcpkgver"
or die "apt-get source failed\n";
unlink $sources or die "failed to unlink $sources\n";
unlink $config or die "failed to unlink $config\n";
make_path(dirname $custom_build_path);
0 == system 'dpkg-source', '--no-check', '--extract',
$dsc_fname, $custom_build_path
or die "dpkg-source failed\n";
if ($cdata->{"Binary-Only-Changes"}) {
open my $infh, '<', "$custom_build_path/debian/changelog"
or die "cannot open debian/changelog for reading: $!\n";
my $changelogcontent = do { local $/; <$infh> };
close $infh;
open my $outfh, '>', "$custom_build_path/debian/changelog"
or die "cannot open debian/changelog for writing: $!\n";
my $logentry = $cdata->{"Binary-Only-Changes"};
# due to storing the binnmu changelog entry in deb822 buildinfo, the
# first character is an unwanted newline
$logentry =~ s/^\n//;
print $outfh $logentry;
# while the linebreak at the beginning is wrong, there are two missing
# at the end
print $outfh "\n\n";
print $outfh $changelogcontent;
close $outfh;
}
0 == system 'env', "--chdir=$custom_build_path", @environment,
'dpkg-buildpackage', '-uc', "--host-arch=$host_arch", "--build=$build"
or die "dpkg-buildpackage failed\n";
# we are not interested in the unpacked source directory
0 == system 'rm', '-r', $custom_build_path
or die "failed to remove $custom_build_path: $?";
# but instead we want the produced artifacts
0 == system 'dcmd', 'mv',
(dirname $custom_build_path)
. "/${srcpkgname}_${srcpkgbinver}_$changesarch.changes", $outdir
or die "dcmd failed\n";
} elsif ($builder eq "sbuild" or $builder eq "sbuild+unshare") {
# we set SBUILD_CONFIG to make sure that the user's ~/.sbuildrc is not
# being used
my ($fh, $sbuildrc)
= tempfile('debrebuild.sbuildrc.XXXXXXXXXXXX', DIR => $tmpdir);
# there might be no apt inside the chroot and we should have all the build
# dependencies installed, so make running apt-get and apt-cache a no-op
print $fh "\$apt_get = '/bin/true';\n";
print $fh "\$apt_cache = '/bin/true';\n";
print $fh "\$build_as_root_when_needed = 1;\n";
close $fh;
my @cmd = (
'env', "--chdir=$outdir", @environment, "SBUILD_CONFIG=$sbuildrc",
'sbuild'
);
push @cmd, "--build=$build_arch";
push @cmd, "--host=$host_arch";
if ($build_source) {
push @cmd, '--source';
} else {
push @cmd, '--no-source';
}
if ($build_archany) {
push @cmd, '--arch-any';
} else {
push @cmd, '--no-arch-any';
}
if ($build_archall) {
push @cmd, '--arch-all';
} else {
push @cmd, '--no-arch-all';
}
if ($cdata->{"Binary-Only-Changes"}) {
push @cmd, "--binNMU-changelog=$cdata->{'Binary-Only-Changes'}";
}
push @cmd, "--chroot=$tarballpath";
push @cmd, "--chroot-mode=unshare";
push @cmd, "--dist=unstable";
push @cmd, "--no-run-lintian";
push @cmd, "--no-run-piuparts";
push @cmd, "--no-run-autopkgtest";
push @cmd, "--no-apt-update";
push @cmd, "--no-apt-upgrade";
push @cmd, "--no-apt-distupgrade";
# Buildinfo files do not indicate whether fakeroot was installed,
# so it is not included in the recreated chroot.
# Since most packages build without issues,
# this simply forces dpkg-buildpackage to run without fakeroot.
# the default was switched in 1.22.13 so no longer needed afterwards.
if ($dpkg_version < Dpkg::Version->new("1.22.13")) {
push @cmd,
"--starting-build-commands="
. 'grep -iq "^Rules-Requires-Root:" "%p/debian/control" || '
. 'sed -i "1iRules-Requires-Root: no" "%p/debian/control"';
}
# without --verbose, the log will be suppressed by default if sbuild is
# not run on an interactive tty, so we make sure the behaviour is always
# the same independent how debrebuild is run
push @cmd, "--verbose";
# since sbuild will always output to stdout, thanks to --verbose, we
# do not need to put the log file to disk anymore. Those interested in the
# log, can just capture stdout of debrebuild
push @cmd, "--nolog";
# disable the explainer
push @cmd, "--bd-uninstallable-explainer=";
if ($custom_build_path) {
my @dirs = File::Spec->splitdir($custom_build_path);
my $build_path = File::Spec->catdir(@dirs[0 .. $#dirs - 1]);
push @cmd, "--build-path=$build_path";
push @cmd, "--dsc-dir=$dirs[-1]";
}
push @cmd, (File::Spec->rel2abs($dsc_fname));
print((join " ", @cmd) . "\n");
0 == system @cmd or die "sbuild failed\n";
unlink $sbuildrc;
} elsif ($builder eq "mmdebstrap") {
my @binnmucmds = ();
if ($cdata->{"Binary-Only-Changes"}) {
my $logentry = $cdata->{"Binary-Only-Changes"};
# due to storing the binnmu changelog entry in deb822 buildinfo, the first
# character is an unwanted newline
$logentry =~ s/^\n//;
# while the linebreak at the beginning is wrong, there are two missing at
# the end
$logentry .= "\n\n";
push @binnmucmds,
'{ printf "%s" '
. (String::ShellQuote::shell_quote $logentry)
. "; cat debian/changelog; } > debian/changelog.debrebuild",
"mv debian/changelog.debrebuild debian/changelog";
}
my @cmd = (
'env', '-i',
'PATH=/usr/sbin:/usr/bin:/sbin:/bin',
'mmdebstrap',
"--arch=$build_arch",
"--variant=custom",
'--skip=setup',
'--skip=update',
'--skip=cleanup',
'--skip=tar-in/mknod',
"--setup-hook=tar-in "
. (String::ShellQuote::shell_quote $tarballpath) . ' /',
'--setup-hook=rm "$1"/etc/apt/sources.list',
(
"--customize-hook=dcmd cp "
. (String::ShellQuote::shell_quote $dsc_fname)
. " \"\$1\""
),
'--customize-hook=chroot "$1" sh -c "'
. (
join ' && ',
"mkdir -p "
. (String::ShellQuote::shell_quote(dirname $custom_build_path)),
"dpkg-source --no-check -x /"
. $srcpkg->get_basename(1) . '.dsc '
. (String::ShellQuote::shell_quote $custom_build_path),
'cd ' . (String::ShellQuote::shell_quote $custom_build_path),
@binnmucmds,
"env $environment dpkg-buildpackage -uc -a $host_arch --build=$build",
'cd /',
'rm -r ' . (String::ShellQuote::shell_quote $custom_build_path))
. '"',
'--customize-hook=sync-out '
. (dirname $custom_build_path)
. " $outdir",
'',
'/dev/null',
);
print((join ' ', @cmd) . "\n");
0 == system @cmd or die "mmdebstrap failed\n";
} else {
die "unsupported builder: $builder\n";
}
# test if all checksums in the buildinfo file check out
if ($builder ne "none") {
print "build artifacts stored in $outdir\n";
my $checksums = Dpkg::Checksums->new();
$checksums->add_from_control($cdata);
# remove the .dsc as we only did the binaries
# - the .dsc cannot be reproduced anyways because we cannot reproduce its
# signature
# - binNMUs can only be done with --build=any
foreach my $file ($checksums->get_files()) {
if ($file !~ /\.dsc$/) {
next;
}
$checksums->remove_file($file);
}
my $new_cdata
= Dpkg::Control->new(type => CTRL_FILE_BUILDINFO, allow_pgp => 1);
$new_cdata->load($new_buildinfo);
my $new_checksums = Dpkg::Checksums->new();
$new_checksums->add_from_control($new_cdata);
my @files = $checksums->get_files();
my @new_files = $new_checksums->get_files();
if (scalar @files != scalar @new_files) {
print("old buildinfo:\n" . (join "\n", @files) . "\n");
print("new buildinfo:\n" . (join "\n", @new_files) . "\n");
die "new buildinfo contains a different number of files\n";
}
for (my $i = 0 ; $i <= $#files ; $i++) {
if ($files[$i] ne $new_files[$i]) {
die "different checksum files at position $i\n";
}
if ($files[$i] =~ /\.dsc$/) {
print("skipping $files[$i]\n");
next;
}
print("checking $files[$i]: ");
if ($checksums->get_size($files[$i])
!= $new_checksums->get_size($files[$i])) {
die "size differs for $files[$i]\n";
} else {
print("size... ");
}
my $chksum = $checksums->get_checksum($files[$i], undef);
my $new_chksum = $new_checksums->get_checksum($new_files[$i], undef);
if (scalar keys %{$chksum} != scalar keys %{$new_chksum}) {
die "different algos for $files[$i]\n";
}
foreach my $algo (keys %{$chksum}) {
if (!exists $new_chksum->{$algo}) {
die "$algo is not used in both buildinfo files\n";
}
if ($chksum->{$algo} ne $new_chksum->{$algo}) {
die "value of $algo differs for $files[$i]\n";
}
print("$algo... ");
}
print("all OK\n");
}
}

138
scripts/debrelease.1 Normal file
View file

@ -0,0 +1,138 @@
.TH DEBRELEASE 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debrelease \- a wrapper around dupload or dput
.SH SYNOPSIS
\fBdebrelease\fR [\fIdebrelease options\fR] [\fIdupload/dput options\fR]
.SH DESCRIPTION
\fBdebrelease\fR is a simple wrapper around \fBdupload\fR or
\fBdput\fR. It is called from within the source code tree of a
package, and figures out the current version of a package. It then
looks for the corresponding \fI.changes\fR file (which lists the files
needed to upload in order to release the package) in the parent
directory of the source code tree and calls \fBdupload\fR or
\fBdput\fR with the \fI.changes\fR file as parameter in order to
perform the actual uploading.
.PP
Options may be given to \fBdebrelease\fR; except for the ones listed
below, they are passed on unchanged to \fBdupload\fR or \fBdput\fR.
The \fBdevscripts\fR configuration files are also read by
\fBdebrelease\fR as described below.
.SH "Directory name checking"
In common with several other scripts in the \fBdevscripts\fR package,
\fBdebrelease\fR will climb the directory tree until it finds a
\fIdebian/changelog\fR file. As a safeguard against stray files
causing potential problems, it will examine the name of the parent
directory once it finds the \fIdebian/changelog\fR file, and check
that the directory name corresponds to the package name. Precisely
how it does this is controlled by two configuration file variables
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR and \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR, and
their corresponding command-line options \fB\-\-check-dirname-level\fR
and \fB\-\-check-dirname-regex\fR.
.PP
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR can take the following values:
.TP
.B 0
Never check the directory name.
.TP
.B 1
Only check the directory name if we have had to change directory in
our search for \fIdebian/changelog\fR. This is the default behaviour.
.TP
.B 2
Always check the directory name.
.PP
The directory name is checked by testing whether the current directory
name (as determined by \fBpwd\fR(1)) matches the regex given by the
configuration file option \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR or by the
command line option \fB\-\-check-dirname-regex\fR \fIregex\fR. Here
\fIregex\fR is a Perl regex (see \fBperlre\fR(3perl)), which will be
anchored at the beginning and the end. If \fIregex\fR contains a '/',
then it must match the full directory path. If not, then it must
match the full directory name. If \fIregex\fR contains the string
\'PACKAGE', this will be replaced by the source package name, as
determined from the changelog. The default value for the regex is:
\'PACKAGE(-.+)?', thus matching directory names such as PACKAGE and
PACKAGE-version.
.SH OPTIONS
.TP
\fB\-\-dupload\fR, \fB\-\-dput\fR
This specifies which uploader program to use; the default is
\fBdupload\fR.
.TP
\fB\-S\fR
If this option is used, or the default \fI.changes\fR file is
not found but a source-only \fI.changes\fR file is present, then this
source-only \fI.changes\fR file will be uploaded instead of an
arch-specific one.
.TP
\fB\-a\fIdebian-architecture\fR, \fB\-t\fIGNU-system-type\fR
See \fBdpkg-architecture\fR(1) for a description of these options.
They affect the search for the \fI.changes\fR file. They are provided
to mimic the behaviour of \fBdpkg-buildpackage\fR when determining the
name of the \fI.changes\fR file. If a plain \fB\-t\fR is given, it is
taken to be the \fBdupload\fR host-specifying option, and therefore
signifies the end of the \fBdebrelease\fR-specific options.
.TP
\fB\-\-multi\fR
Multiarch \fI.changes\fR mode: This signifies that \fBdebrelease\fR should
use the most recent file with the name pattern
\fIpackage_version_*+*.changes\fR as the \fI.changes\fR file, allowing for the
\fI.changes\fR files produced by \fBdpkg-cross\fR.
.TP
\fB\-\-debs\-dir\fR \fIdirectory\fR
Look for the \fI.changes\fR and \fI.deb\fR files in \fIdirectory\fR
instead of the parent of the source directory. This should
either be an absolute path or relative to the top of the source
directory.
.TP
\fB\-\-check-dirname-level\fR \fIN\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-check-dirname-regex\fR \fIregex\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
.BR \-\-help ", " \-h
Display a help message and exit successfully.
.TP
.B \-\-version
Display version and copyright information and exit successfully.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variables are:
.TP
.B DEBRELEASE_UPLOADER
The currently recognised values are \fIdupload\fR and \fIdput\fR, and
it specifies which uploader program should be used. It corresponds to
the \fB\-\-dupload\fR and \fB\-\-dput\fR command line options.
.TP
.B DEBRELEASE_DEBS_DIR
This specifies the directory in which to look for the \fI.changes\fR
and \fI.deb\fR files, and is either an absolute path or relative to
the top of the source tree. This corresponds to the
\fB\-\-debs\-dir\fR command line option. This directive could be
used, for example, if you always use \fBpbuilder\fR or
\fBsvn-buildpackage\fR to build your packages. Note that it also
affects \fBdebc\fR(1) and \fBdebi\fR(1).
.TP
.BR DEVSCRIPTS_CHECK_DIRNAME_LEVEL ", " DEVSCRIPTS_CHECK_DIRNAME_REGEX
See the above section \fBDirectory name checking\fR for an explanation of
these variables. Note that these are package-wide configuration
variables, and will therefore affect all \fBdevscripts\fR scripts
which check their value, as described in their respective manpages and
in \fBdevscripts.conf\fR(5).
.SH "SEE ALSO"
.BR dput (1),
.BR dupload (1),
.BR devscripts.conf (5)
.SH AUTHOR
Julian Gilbey <jdg@debian.org>, based on the original \fBrelease\fR
script by Christoph Lameter <clameter@debian.org>.

341
scripts/debrelease.sh Executable file
View file

@ -0,0 +1,341 @@
#!/bin/bash
# debrelease: a devscripts wrapper around dupload/dput which calls
# dupload/dput with the correct .changes file as parameter.
# All command line options are passed onto dupload.
#
# Written and copyright 1999-2003 by Julian Gilbey <jdg@debian.org>
# Based on the original 'release' script by
# Christoph Lameter <clameter@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -e
PROGNAME=${0##*/}
MODIFIED_CONF_MSG='Default settings modified by devscripts configuration files:'
usage() {
echo \
"Usage: $PROGNAME [debrelease options] [dupload/dput options]
Run dupload on the newly created changes file.
Debrelease options:
--dupload Use dupload to upload files (default)
--dput Use dput to upload files
-a<arch> Search for .changes file made for Debian build <arch>
-t<target> Search for .changes file made for GNU <target> arch
-S Search for source-only .changes file instead of arch one
--multi Search for multiarch .changes file made by dpkg-cross
--debs-dir DIR Look for the changes and debs files in DIR instead of
the parent of the current package directory
--check-dirname-level N
How much to check directory names before cleaning trees:
N=0 never
N=1 only if program changes directory (default)
N=2 always
--check-dirname-regex REGEX
What constitutes a matching directory name; REGEX is
a Perl regular expression; the string \`PACKAGE' will
be replaced by the package name; see manpage for details
(default: 'PACKAGE(-.+)?')
--no-conf, --noconf
Don't read devscripts config files;
must be the first option given
--help Show this message
--version Show version and copyright information
$MODIFIED_CONF_MSG"
}
version() {
echo \
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
This code is copyright 1999-2003 by Julian Gilbey, all rights reserved.
Based on original code by Christoph Lameter.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later."
}
mustsetvar() {
if [ "x$2" = x ]
then
echo >&2 "$PROGNAME: unable to determine $3"
exit 1
else
# echo "$PROGNAME: $3 is $2"
eval "$1=\"\$2\""
fi
}
# Boilerplate: set config variables
DEFAULT_DEBRELEASE_UPLOADER=dupload
DEFAULT_DEBRELEASE_DEBS_DIR=..
DEFAULT_DEVSCRIPTS_CHECK_DIRNAME_LEVEL=1
DEFAULT_DEVSCRIPTS_CHECK_DIRNAME_REGEX='PACKAGE(-.+)?'
VARS="DEBRELEASE_UPLOADER DEBRELEASE_DEBS_DIR DEVSCRIPTS_CHECK_DIRNAME_LEVEL DEVSCRIPTS_CHECK_DIRNAME_REGEX"
if [ "$1" = "--no-conf" -o "$1" = "--noconf" ]; then
shift
MODIFIED_CONF_MSG="$MODIFIED_CONF_MSG
(no configuration files read)"
# set defaults
for var in $VARS; do
eval "$var=\$DEFAULT_$var"
done
else
# Run in a subshell for protection against accidental errors
# in the config files
eval $(
set +e
for var in $VARS; do
eval "$var=\$DEFAULT_$var"
done
for file in /etc/devscripts.conf ~/.devscripts
do
[ -r $file ] && . $file
done
set | grep -E "^(DEBRELEASE|DEVSCRIPTS)_")
# check sanity
case "$DEBRELEASE_UPLOADER" in
dupload|dput) ;;
*) DEBRELEASE_UPLOADER=dupload ;;
esac
# We do not replace this with a default directory to avoid accidentally
# uploading a broken package
DEBRELEASE_DEBS_DIR="$(echo "$DEBRELEASE_DEBS_DIR" | sed -e 's%/\+%/%g; s%\(.\)/$%\1%;')"
if ! [ -d "$DEBRELEASE_DEBS_DIR" ]; then
debsdir_warning="config file specified DEBRELEASE_DEBS_DIR directory $DEBRELEASE_DEBS_DIR does not exist!"
fi
case "$DEVSCRIPTS_CHECK_DIRNAME_LEVEL" in
0|1|2) ;;
*) DEVSCRIPTS_CHECK_DIRNAME_LEVEL=1 ;;
esac
# set config message
MODIFIED_CONF=''
for var in $VARS; do
eval "if [ \"\$$var\" != \"\$DEFAULT_$var\" ]; then
MODIFIED_CONF_MSG=\"\$MODIFIED_CONF_MSG
$var=\$$var\";
MODIFIED_CONF=yes;
fi"
done
if [ -z "$MODIFIED_CONF" ]; then
MODIFIED_CONF_MSG="$MODIFIED_CONF_MSG
(none)"
fi
fi
# synonyms
CHECK_DIRNAME_LEVEL="$DEVSCRIPTS_CHECK_DIRNAME_LEVEL"
CHECK_DIRNAME_REGEX="$DEVSCRIPTS_CHECK_DIRNAME_REGEX"
sourceonly=
multiarch=
debsdir="$DEBRELEASE_DEBS_DIR"
while [ $# -gt 0 ]
do
case "$1" in
-a*) targetarch="$(echo "$1" | sed -e 's/^-a//')" ;;
-t*) targetgnusystem="$(echo "$1" | sed -e 's/^-t//')"
# dupload has a -t option
if [ -z "$targetgnusystem" ]; then break; fi ;;
-S) sourceonly=source ;;
--multi) multiarch=yes ;;
--dupload) DEBRELEASE_UPLOADER=dupload ;;
--dput) DEBRELEASE_UPLOADER=dput ;;
# Delay checking of debsdir until we need it. We need to make sure we're
# in the package root directory first.
--debs-dir=*)
opt_debsdir="$(echo "$1" | sed -e 's/^--debs-dir=//; s%/\+%/%g; s%\(.\)/$%\1%;')"
;;
--debs-dir)
shift
opt_debsdir="$(echo "$1" | sed -e 's%/\+%/%g; s%\(.\)/$%\1%;')"
;;
--check-dirname-level=*)
level="$(echo "$1" | sed -e 's/^--check-dirname-level=//')"
case "$level" in
0|1|2) CHECK_DIRNAME_LEVEL=$level ;;
*) echo "$PROGNAME: unrecognised --check-dirname-level value (allowed are 0,1,2)" >&2
exit 1 ;;
esac
;;
--check-dirname-level)
shift
case "$1" in
0|1|2) CHECK_DIRNAME_LEVEL=$1 ;;
*) echo "$PROGNAME: unrecognised --check-dirname-level value (allowed are 0,1,2)" >&2
exit 1 ;;
esac
;;
--check-dirname-regex=*)
regex="$(echo "$1" | sed -e 's/^--check-dirname-level=//')"
if [ -z "$regex" ]; then
echo "$PROGNAME: missing --check-dirname-regex parameter" >&2
echo "try $PROGNAME --help for usage information" >&2
exit 1
else
CHECK_DIRNAME_REGEX="$regex"
fi
;;
--check-dirname-regex)
shift;
if [ -z "$1" ]; then
echo "$PROGNAME: missing --check-dirname-regex parameter" >&2
echo "try $PROGNAME --help for usage information" >&2
exit 1
else
CHECK_DIRNAME_REGEX="$1"
fi
;;
--no-conf|--noconf)
echo "$PROGNAME: $1 is only acceptable as the first command-line option!" >&2
exit 1 ;;
--dopts) shift; break ;; # This is an option for cvs-debrelease,
# so we accept it here too, even though we don't
# advertise it
--help) usage; exit 0 ;;
--version) version; exit 0 ;;
*) break ;; # a dupload/dput option, so stop parsing here
esac
shift
done
# Look for .changes file via debian/changelog
CHDIR=
until [ -f debian/changelog ]; do
CHDIR=yes
cd ..
if [ $(pwd) = "/" ]; then
echo "$PROGNAME: cannot find debian/changelog anywhere!" >&2
echo "Are you in the source code tree?" >&2
exit 1
fi
done
# Use svn-buildpackage's directory if there is one and debsdir wasn't already
# specified on the command-line. This can override DEBRELEASE_DEBS_DIR.
if [ -n "$opt_debsdir" ]; then
debsdir="$opt_debsdir"
elif [ -e ".svn/deb-layout" ]; then
buildArea="$(sed -ne '/^buildArea=/{s/^buildArea=//; s%/\+%/%g; s%\(.\)/$%\1%; p; q}' .svn/deb-layout)"
if [ -n "$buildArea" -a -d "$buildArea" ]; then
debsdir="$buildArea"
fi
fi
# check sanity of debsdir
if ! [ -d "$debsdir" ]; then
if [ -n "$debsdir_warning" ]; then
echo "$PROGNAME: $debsdir_warning" >&2
exit 1
else
echo "$PROGNAME: could not find directory $debsdir!" >&2
exit 1
fi
fi
mustsetvar package "`dpkg-parsechangelog -SSource`" "source package"
mustsetvar version "`dpkg-parsechangelog -SVersion`" "source version"
if [ $CHECK_DIRNAME_LEVEL -eq 2 -o \
\( $CHECK_DIRNAME_LEVEL -eq 1 -a "$CHDIR" = yes \) ]; then
if ! perl -MFile::Basename -w \
-e "\$pkg='$package'; \$re='$CHECK_DIRNAME_REGEX';" \
-e '$re =~ s/PACKAGE/\\Q$pkg\\E/g; $pwd=`pwd`; chomp $pwd;' \
-e 'if ($re =~ m%/%) { eval "exit (\$pwd =~ /^$re\$/ ? 0:1);"; }' \
-e 'else { eval "exit (basename(\$pwd) =~ /^$re\$/ ? 0:1);"; }'
then
echo >&2 <<EOF
$PROGNAME: found debian/changelog for package $PACKAGE in the directory
$pwd
but this directory name does not match the package name according to the
regex $check_dirname_regex.
To run $PROGNAME on this package, see the --check-dirname-level and
--check-dirname-regex options; run $PROGNAME --help for more info.
EOF
exit 1
fi
fi
if [ "x$sourceonly" = "xsource" ]; then
arch=source
elif [ -n "$targetarch" ] && [ -n "$targetgnusystem" ]; then
mustsetvar arch "$(dpkg-architecture "-a${targetarch}" "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture"
elif [ -n "$targetarch" ]; then
mustsetvar arch "$(dpkg-architecture "-a${targetarch}" -qDEB_HOST_ARCH)" "build architecture"
elif [ -n "$targetgnusystem" ]; then
mustsetvar arch "$(dpkg-architecture "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture"
else
mustsetvar arch "$(dpkg-architecture -qDEB_HOST_ARCH)" "build architecture"
fi
sversion=$(echo "$version" | perl -pe 's/^\d+://')
pva="${package}_${sversion}_${arch}"
pvs="${package}_${sversion}_source"
changes="$debsdir/$pva.changes"
schanges="$debsdir/$pvs.changes"
mchanges=$(ls "$debsdir/${package}_${sversion}_*+*.changes" "$debsdir/${package}_${sversion}_multi.changes" 2>/dev/null | head -1)
if [ -n "$multiarch" ]; then
if [ -z "$mchanges" -o ! -r "$mchanges" ]; then
echo "$PROGNAME: could not find/read any multiarch .changes file with name" >&2
echo "$debsdir/${package}_${sversion}_*.changes" >&2
exit 1
fi
changes=$mchanges
elif [ "$arch" = source ]; then
if [ -r "$schanges" ]; then
changes=$schanges
else
echo "$PROGNAME: could not find/read changes file $schanges!" >&2
exit 1
fi
else
if [ ! -r "$changes" ]; then
if [ -r "$mchanges" ]; then
changes=$mchanges
echo "$PROGNAME: could only find a multiarch changes file:" >&2
echo " $mchanges" >&2
echo -n "Should I upload this file? (y/n) " >&2
read ans
case ans in
y*) ;;
*) exit 1 ;;
esac
else
echo "$PROGNAME: could not read changes file $changes!" >&2
exit 1
fi
fi
fi
exec $DEBRELEASE_UPLOADER "$@" "$changes"
echo "$PROGNAME: failed to exec $DEBRELEASE_UPLOADER!" >&2
echo "Aborting...." >&2
exit 1

177
scripts/debrepro.pod Normal file
View file

@ -0,0 +1,177 @@
=head1 NAME
debrepro - reproducibility tester for Debian packages
=head1 SYNOPSIS
B<debrepro> [I<OPTIONS>] [I<SOURCEDIR>]
=head1 DESCRIPTION
B<debrepro> will build a given source directory twice, with a set of
variations between the first and the second build, and compare the
produced binary packages. If B<diffoscope> is installed, it is used to
compare non-matching binaries. If B<disorderfs> is installed, it is used
during the build to inject non-determinism in filesystem listing
operations.
I<SOURCEDIR> must be a directory containing an unpacked Debian source
package. If I<SOURCEDIR> is omitted, the current directory is assumed.
=head1 OUTPUT DIRECTORY
At the very end of a build, B<debrepro> will inform the location of the
output directory where the build artifacts can be found. In that
directory, you will find:
=over
=item I<$OUTPUTDIR/first>
Contains the results of the first build, including a copy of the source
tree, and the resulting binary packages.
=item I<$OUTPUTDIR/first/build.sh>
Contains the exact build script that was used in the first build.
=item I<$OUTPUTDIR/second>
Contains the results of the second build, including a copy of the source tree,
and the resulting binary packages.
=item I<$OUTPUTDIR/second/build.sh>
Contains the exact build script that was used in the second build.
=back
Taking a B<diff(1)> between I<$OUTPUTDIR/first/build.sh> and
I<$OUTPUTDIR/second/build.sh> is an excellent way of figuring out
exactly what changed between the two builds.
=head1 SUPPORTED VARIATIONS
=over
=item B<user>
The I<$USER> environment variable will contain different values between the
first and second builds.
=item B<path>
During the second build, a fake, non-existing directory will be appended to the
I<$PATH> environment variable.
=item B<umask>
The builds will use different umask settings.
=item B<locale>
Both I<$LC_ALL> and I<$LANG> will be different across the two builds.
=item B<timezone>
I<$TZ> will be different across builds.
=item B<filesystem-ordering>
If B<disorderfs> is installed, both builds will be done under a disorderfs
overlay directory. This will cause filesystem listing operations to be return
items in a non-deterministic order.
=item B<time>
The second build will be executed 213 days, 7 hours and 13 minutes in the
future with regards to the current time (using B<faketime(1)>).
=back
=head1 OPTIONS
=over
=item -s VARIATION, --skip VARIATION
Don't perform the named VARIATION. Variation names are the ones used in
their description in section B<SUPPORTED VARIATIONS>.
=item -b COMMAND, --before-second-build COMMAND
Run COMMAND before performing the second build. This can be used for
example to apply a patch to a source tree for the second build, and
check whether (or how) the resulting binaries are affected.
Examples:
$ debrepro --before-second-build "git checkout branch-with-changes"
$ debrepro --before-second-build "patch -p1 < /path/to/patch"
=item -B COMMAND, --build-command COMMAND
Use custom build command. Default: I<dpkg-buildpackage -b -us -uc>.
If a custom build command is specified, the restriction of only running
against a Debian source tree is relaxed and you can run debrepro against
any source directory.
=item -a PATTERN, --artifact-pattern PATTERN
Define a file glob pattern to determine which artifacts need to be
compared across the builds. Default: I<../*.deb>.
=item -n, --no-copy
Do not copy the source directory to the temporary work directory before
each build. Use this to run debrepro against the source directory
directly.
=item -t TIME, --timeout TIME
Apply a timeout to all builds. I<TIME> must be a time specification
compatible with GNU timeout(1).
=item -h, --help
Display this help message and exit.
=back
=head1 EXIT STATUS
=over
=item 0Z<>
Package is reproducible.
Reproducible here means that the two builds produced the exactly the
same binaries, under the set of variations that B<debrepro> tests. Other
sources of non-determinism in builds that are not yet tested might still
affect builds in the wild.
=item 1Z<>
Package is not reproducible.
=item 2Z<>
The given input is not a valid Debian source package.
=item 3Z<>
Required programs are missing.
=back
=head1 SEE ALSO
diffoscope (1), disorderfs (1), timeout(1)
=head1 AUTHOR
Antonio Terceiro <terceiro@debian.org>.

293
scripts/debrepro.sh Executable file
View file

@ -0,0 +1,293 @@
#!/bin/sh
# debrepro: a reproducibility tester for Debian packages
#
# © 2016 Antonio Terceiro <terceiro@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -eu
check_dependencies() {
for optional in disorderfs diffoscope; do
if ! command -v "$optional" > /dev/null; then
echo "W: $optional not installed, there will be missing functionality" >&2
fi
done
local failed=''
for mandatory in faketime; do
if ! command -v "$mandatory" > /dev/null; then
echo "E: $mandatory not installed, cannot proceed." >&2
failed=yes
fi
done
if [ -n "$failed" ]; then
exit 3
fi
}
usage() {
echo "usage: $0 [OPTIONS] [SOURCEDIR]"
echo ""
echo "Options:"
echo ""
echo " -b,--before-second-build COMMAND Run COMMAND before second build"
echo " (e.g. apply a patch)"
echo " -B, --build-command COMMAND Use COMMAND as the build command"
echo " (default: dpkg-buildpackage -b -us -uc)"
echo " -a, --artifact-pattern Shell glob pattern to determine which"
echo " artifacts should be compared across the"
echo " different builds (default: ../*.deb)"
echo " -n, --no-copy Does not copy the source tree before"
echo " each build; run commands directly in the"
echo " source tree."
echo " -s,--skip VARIATION Don't perform the named variation"
echo " -h,--help Display this help message and exit"
}
first_banner=y
banner() {
if [ "$first_banner" = n ]; then
echo
fi
echo "$@" | sed -e 's/./=/g'
echo "$@"
echo "$@" | sed -e 's/./=/g'
echo
first_banner=n
}
variation() {
echo
echo "# Variation:" "$@"
}
vary() {
local var="$1"
for skipped in $skip_variations; do
if [ "$skipped" = "$var" ]; then
return
fi
done
variation "$var"
local first="$2"
local second="$3"
if [ "$which_build" = 'first' ]; then
if [ -n "$first" ]; then
echo "$first"
fi
else
echo "$second"
fi
}
create_build_script() {
echo 'set -eu'
echo
echo "# this script must be run from inside an unpacked Debian source"
echo "# package"
echo
vary path \
'' \
'export PATH="$PATH":/i/capture/the/path'
vary user \
'export USER=user1' \
'export USER=user2'
vary umask \
'umask 0022' \
'umask 0002'
vary locale \
'export LC_ALL=C.UTF-8 LANG=C.UTF-8' \
'export LC_ALL=pt_BR.UTF-8 LANG=pt_BR.UTF-8'
vary timezone \
'export TZ=GMT+12' \
'export TZ=GMT-14'
if command -v disorderfs >/dev/null; then
disorderfs_commands='cd .. &&
mv source orig &&
mkdir source &&
disorderfs --shuffle-dirents=yes orig source &&
trap "cd .. && fusermount -u source && rmdir source && mv orig source" INT TERM EXIT &&
cd source'
vary filesystem-ordering \
'' \
"$disorderfs_commands"
fi
echo 'build_prefix=""'
vary time \
'' \
'build_prefix="faketime +213days+7hours+13minutes"; export NO_FAKE_STAT=1'
if [ -n "$timeout" ]; then
echo "build_prefix=\"timeout $timeout \$build_prefix\""
fi
echo '${build_prefix:-} '"${build_command:-dpkg-buildpackage -b -us -uc}"
}
build() {
export which_build="$1"
mkdir "$tmpdir/build"
if [ "${copy}" = yes ]; then
cp -r "$SOURCE" "$tmpdir/build/source"
cd "$tmpdir/build/source"
fi
if [ "$which_build" = second ] && [ -n "$before_second_build_command" ]; then
banner "I: running before second build: $before_second_build_command"
sh -c "$before_second_build_command"
fi
create_build_script > $tmpdir/build/build.sh
if ! sh $tmpdir/build/build.sh; then
echo "E: $which_build build failed"
exit 1
fi
mkdir -p $tmpdir/build/artifacts
cp ${artifact_pattern} $tmpdir/build/artifacts/ || true
if [ "${copy}" = yes ]; then
cd - > /dev/null
fi
mv "$tmpdir/build" "$tmpdir/$which_build"
}
binmatch() {
cmp --silent "$1" "$2"
}
compare() {
rc=0
diff=binmatch
if command -v diffoscope >/dev/null; then
diff=diffoscope
fi
for first_artifact in "$tmpdir"/first/artifacts/${artifact_pattern}; do
artifact_name="$(basename "$first_artifact")"
second_artifact="$tmpdir"/second/artifacts/"$artifact_name"
if [ ! -f "${first_artifact}" ]; then
echo "$artifact_name: not found"
rc=1
elif ${diff} "$first_artifact" "$second_artifact"; then
echo "$artifact_name: files match"
else
echo "$artifact_name: files don't match"
rc=1
fi
done
if [ "$rc" -ne 0 ]; then
echo "E: package is not reproducible."
fi
return "$rc"
}
TEMP=$(getopt -n "debrepro" -o 'hs:b:B:a:nft:' \
-l 'help,skip:,before-second-build:,build-command:,artifact-pattern:,no-copy,force,timeout:' \
-- "$@") || (rc=$?; usage >&2; exit $rc)
eval set -- "$TEMP"
skip_variations=""
before_second_build_command=''
timeout=''
build_command=''
artifact_pattern="../*.deb"
copy=yes
while true; do
case "$1" in
-s|--skip)
case "$2" in
user|path|umask|locale|timezone|filesystem-ordering|time)
skip_variations="$skip_variations $2"
;;
*)
echo "E: invalid variation name $2"
exit 1
;;
esac
shift
;;
-b|--before-second-build)
before_second_build_command="$2"
shift
;;
-B|--build-command)
build_command="$2"
shift
;;
-a|--artifact-pattern)
artifact_pattern="$2"
shift
;;
-n|--no-copy)
copy=no
skip_variations="$skip_variations filesystem-ordering"
;;
-t|--timeout)
timeout="$2"
shift
;;
-h|--help)
usage
exit
;;
--)
shift
break
;;
esac
shift
done
SOURCE="${1:-}"
if [ -z "$SOURCE" ]; then
SOURCE="$(pwd)"
fi
if [ ! -f "$SOURCE/debian/changelog" ]; then
if [ -n "${build_command}" ]; then
echo "W: $SOURCE does not look like a Debian source package, but proceeding anyway since a custom build command as provided"
else
echo "E: $SOURCE does not look like a Debian source package"
exit 2
fi
fi
tmpdir=$(mktemp --directory --tmpdir debrepro.XXXXXXXXXX)
trap "if [ \$? -eq 0 ]; then rm -rf $tmpdir; else echo; echo 'I: artifacts left in $tmpdir'; fi" INT TERM EXIT
check_dependencies
banner "First build"
build first
banner "Second build"
build second
banner "Comparing artifacts"
compare first second
# vim:ts=4 sw=4 et

72
scripts/debrsign.1 Normal file
View file

@ -0,0 +1,72 @@
.TH DEBRSIGN 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debrsign \- remotely sign a Debian .changes and .dsc file pair using SSH
.SH SYNOPSIS
\fBdebrsign\fR [\fIoptions\fR] [\fIuser\fB@\fR]\fIremotehost\fR
[\fIchanges-file\fR|\fIdsc-file\fR]
.SH DESCRIPTION
\fBdebrsign\fR takes either an unsigned \fI.dsc\fR file or an
unsigned \fI.changes\fR file and the associated unsigned \fI.dsc\fR
file (found by replacing the architecture name and \fI.changes\fR
by \fI.dsc\fR) if it appears in the \fI.changes\fR file and signs them
by copying them to the remote machine using \fBssh\fR(1) and remotely
running \fBdebsign\fR(1) on that machine. All options not listed
below are passed to the \fBdebsign\fR program on the remote machine.
.PP
If a \fI.changes\fR or \fI.dsc\fR file is specified, it is signed,
otherwise, \fIdebian/changelog\fR is parsed to determine the name of
the \fI.changes\fR file to look for in the parent directory.
.PP
This utility is useful if a developer must build a package on one
machine where it is unsafe to sign it; they need then only transfer
the small \fI.dsc\fR and \fI.changes\fR files to a safe machine and
then use the \fBdebsign\fR program to sign them before
transferring them back. This program automates this process.
.PP
To do it the other way round, that is to connect to an unsafe machine
to download the \fI.dsc\fR and \fI.changes\fR files, to sign them
locally and then to transfer them back, see the \fBdebsign\fR(1)
program, which can do this task.
.SH OPTIONS
.TP
\fB\-S\fR
Look for a source-only \fI.changes\fR file instead of a binary-build
\fI.changes\fR file.
.TP
\fB\-a\fIdebian-architecture\fR, \fB\-t\fIGNU-system-type\fR
See \fBdpkg-architecture\fR(1) for a description of these options.
They affect the search for the \fI.changes\fR file. They are provided
to mimic the behaviour of \fBdpkg-buildpackage\fR when determining the
name of the \fI.changes\fR file.
.TP
\fB\-\-multi\fR
Multiarch \fI.changes\fR mode: This signifies that \fBdebrsign\fR should
use the most recent file with the name pattern
\fIpackage_version_*+*.changes\fR as the \fI.changes\fR file, allowing for the
\fI.changes\fR files produced by \fBdpkg-cross\fR.
.TP
\fB\-\-path \fIremote-path\fR
Specify a path to the GPG binary on the remote host.
.TP
\fB\-\-help\fR, \fB\-\-version\fR
Show help message and version information respectively.
.TP
\fBOther options\fR
All other options are passed on to \fBdebsign\fR on the remote
machine.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variables are:
.TP
.B DEBRSIGN_PGP_PATH
Equivalent to passing \fB\-\-path\fR on the command line (see above.)
.SH "SEE ALSO"
.BR debsign (1),
.BR dpkg-architecture (1),
.BR ssh (1)
.SH AUTHOR
This program was written by Julian Gilbey <jdg@debian.org> and is
copyright under the GPL, version 2 or later.

273
scripts/debrsign.sh Executable file
View file

@ -0,0 +1,273 @@
#!/bin/bash
# This program is used to REMOTELY sign a .dsc and .changes file
# pair in the form needed for a legal Debian upload. It is based on
# dpkg-buildpackage and debsign (which is also part of the devscripts
# package).
#
# In order for this program to work, debsign must be installed
# on the REMOTE machine which will be used to sign your package.
# You should run this program from within the package directory on
# the build machine.
#
# Debian GNU/Linux debrsign.
# Copyright 1999 Mike Goldman, all rights reserved
# Modifications copyright 1999 Julian Gilbey <jdg@debian.org>,
# all rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# Abort if anything goes wrong
set -e
PROGNAME=${0##*/}
usage() {
echo \
"Usage: debrsign [options] [username@]remotehost [changes or dsc]
Options:
-p<sign-command> The command to use for signing
-e<maintainer> Sign using key of <maintainer> (takes precedence over -m)
-m<maintainer> The same as -e
-k<keyid> The key to use for signing
-S Use changes file made for source-only upload
-a<arch> Use changes file made for Debian target architecture <arch>
-t<target> Use changes file made for GNU target architecture <target>
--multi Use most recent multiarch .changes file found
--path Specify directory GPG binary is located on remote host
--help Show this message
--version Show version and copyright information
If a changes or dscfile is specified, it is signed, otherwise
debian/changelog is parsed to find the changes file. The signing
is performed on remotehost using ssh and debsign."
}
version() {
echo \
"This is debrsign, from the Debian devscripts package, version ###VERSION###
This code is copyright 1999 by Mike Goldman and Julian Gilbey,
all rights reserved. This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later."
}
mustsetvar() {
if [ "x$2" = x ]
then
echo >&2 "$PROGNAME: unable to determine $3"
exit 1
else
# echo "$PROGNAME: $3 is $2"
eval "$1=\"\$2\""
fi
}
withecho() {
echo " $@"
"$@"
}
# --- main script
# For partial security, even though we know it doesn't work :(
# I guess maintainers will have to be careful, and there's no way around
# this in a shell script.
unset IFS
PATH=/usr/local/bin:/usr/bin:/bin
umask $(perl -e 'printf "%03o\n", umask | 022')
eval $(
set +e
for var in $VARS; do
eval "$var=\$DEFAULT_$var"
done
for file in /etc/devscripts.conf ~/.devscripts; do
[ -r $file ] && . $file
done
set | grep '^DEBRSIGN_')
signargs=
while [ $# != 0 ]
do
value="$(echo x"$1" | sed -e 's/^x-.//')"
case "$1" in
-S) sourceonly="true" ;;
-a*) targetarch="$value" ;;
-t*) targetgnusystem="$value" ;;
--multi) multiarch="true" ;;
--help) usage; exit 0 ;;
--version) version; exit 0 ;;
--path) DEBRSIGN_PGP_PATH="$value" ;;
-*) signargs="$signargs '$1'" ;;
*) break ;;
esac
shift
done
# Command line parameters are remote host (mandatory) and changes file
# name (optional). If there is no changes file name, we must be at the
# top level of a source tree and will figure out its name from
# debian/changelog
case $# in
2) remotehost="$1"
case "$2" in
*.dsc)
changes=
dsc=$2
;;
*.changes)
changes=$2
dsc=$(echo $changes | \
perl -pe 's/\.changes$/.dsc/; s/(.*)_(.*)_(.*)\.dsc/\1_\2.dsc/')
buildinfo=$(echo $changes | \
perl -pe 's/\.changes$/.buildinfo/; s/(.*)_(.*)_(.*)\.buildinfo/\1_\2_\3.buildinfo/')
;;
*)
echo "$PROGNAME: Only a .changes or .dsc file is allowed as second argument!" >&2
exit 1
;;
esac
;;
1) remotehost="$1"
case "$1" in
*.changes)
echo "$PROGNAME: You must pass the address of the signing host as as the first argument" >&2
exit 1
;;
*)
# We have to parse debian/changelog to find the current version
if [ ! -r debian/changelog ]; then
echo "$PROGNAME: Must be run from top of source dir or a .changes file given as arg" >&2
exit 1
fi
;;
esac
mustsetvar package "`dpkg-parsechangelog -SSource`" "source package"
mustsetvar version "`dpkg-parsechangelog -SVersion`" "source version"
if [ "x$sourceonly" = x ]
then
if [ -n "$targetarch" ] && [ -n "$targetgnusystem" ]; then
mustsetvar arch "$(dpkg-architecture "-a${targetarch}" "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture"
elif [ -n "$targetarch" ]; then
mustsetvar arch "$(dpkg-architecture "-a${targetarch}" -qDEB_HOST_ARCH)" "build architecture"
elif [ -n "$targetgnusystem" ]; then
mustsetvar arch "$(dpkg-architecture "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture"
else
mustsetvar arch "$(dpkg-architecture -qDEB_HOST_ARCH)" "build architecture"
fi
else
arch=source
fi
sversion=$(echo "$version" | perl -pe 's/^\d+://')
pv="${package}_${sversion}"
pva="${package}_${sversion}${arch:+_${arch}}"
dsc="../$pv.dsc"
buildinfo="../$pva.buildinfo"
changes="../$pva.changes"
if [ -n "$multiarch" -o ! -r $changes ]; then
changes=$(ls "../${package}_${sversion}_*+*.changes" "../${package}_${sversion}_multi.changes" 2>/dev/null | head -1)
if [ -z "$multiarch" ]; then
if [ -n "$changes" ]; then
echo "$PROGNAME: could not find normal .changes file but found multiarch file:" >&2
echo " $changes" >&2
echo "Using this changes file instead." >&2
else
echo "$PROGNAME: Can't find or can't read changes file $changes!" >&2
exit 1
fi
elif [ -n "$multiarch" -a -z "$changes" ]; then
echo "$PROGNAME: could not find any multiarch .changes file with name" >&2
echo "../${package}_${sversion}_*.changes" >&2
exit 1
fi
fi
;;
*) echo "Usage: $PROGNAME [options] [user@]remotehost [.changes or .dsc file]" >&2
exit 1 ;;
esac
if [ "x$remotehost" == "x" ]
then
echo "No [user@]remotehost specified!" >&2
exit 1
fi
declare -A base
base["$changes"]=$(basename "$changes")
base["$dsc"]=$(basename "$dsc")
base["$buildinfo"]=$(basename "$buildinfo")
if [ -n "$changes" ]
then
if [ ! -f "$changes" -o ! -r "$changes" ]
then
echo "Can't find or can't read changes file $changes!" >&2
exit 1
fi
# Is there a dsc file listed in the changes file?
if grep -q "${base[$dsc]}" "$changes"
then
if [ ! -f "$dsc" -o ! -r "$dsc" ]
then
echo "Can't find or can't read dsc file $dsc!" >&2
exit 1
fi
else
unset base["$dsc"]
fi
# Is there a buildinfo file listed in the changes file?
if grep -q "${base[$buildinfo]}" "$changes"
then
if [ ! -f "$buildinfo" -o ! -r "$buildinfo" ]
then
echo "Can't find or can't read buildinfo file $buildinfo!" >&2
exit 1
fi
else
unset base["$buildinfo"]
fi
# Now do the real work
withecho scp "${!base[@]}" "$remotehost:\$HOME"
withecho ssh -t "$remotehost" "debsign $signargs ${base[$changes]}"
for file in "${!base[@]}"
do
withecho scp "$remotehost:\$HOME/${base["$file"]}" "$file"
done
withecho ssh "$remotehost" "rm -f ${base[@]}"
echo "Successfully signed changes file"
else
if [ ! -f "$dsc" -o ! -r "$dsc" ]
then
echo "Can't find or can't read dsc file $dsc!" >&2
exit 1
fi
withecho scp "$dsc" "$remotehost:\$HOME"
withecho ssh -t "$remotehost" "${DEBRSIGN_PGP_PATH}debsign $signargs $dscbase"
withecho scp "$remotehost:\$HOME/$dscbase" "$dsc"
withecho ssh "$remotehost" "rm -f $dscbase"
echo "Successfully signed dsc file"
fi
exit 0

145
scripts/debsign.1 Normal file
View file

@ -0,0 +1,145 @@
.TH DEBSIGN 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debsign \- sign a Debian .changes and .dsc file pair using GPG
.SH SYNOPSIS
\fBdebsign\fR [\fIoptions\fR] [\fIchanges-file\fR|\fIdsc-file\fR|\fIcommands-file\fR ...]
.SH DESCRIPTION
\fBdebsign\fR mimics the signing aspects (and bugs) of
\fBdpkg-buildpackage\fR(1). It takes a \fI.dsc\fR, \fI.buildinfo\fR, or
\fI.changes\fR file and signs it, and any child \fI.dsc\fR,
\fI.buildinfo\fR, or \fI.changes\fR files directly or indirectly
referenced by it, using the GNU Privacy Guard. It is careful to
calculate the size and checksums of any newly signed child files and
replace the original values in the parent file.
.PP
If no file is specified, \fIdebian/changelog\fR is parsed to determine
the name of the \fI.changes\fR file to look for in the parent
directory.
.PP
If a \fI.commands\fR file is specified it is first validated (see the
details at \fIftp://ftp.upload.debian.org/pub/UploadQueue/README\fR),
and the name specified in the Uploader field is used for signing.
.PP
This utility is useful if a developer must build a package on one
machine where it is unsafe to sign it; they need then only transfer
the small \fI.dsc\fR, \fI.buildinfo\fR and \fI.changes\fR files to a
safe machine and then use the \fBdebsign\fR program to sign them before
transferring them back. This process can be automated in two ways.
If the files to be signed live on the \fBremote\fR machine, the
\fB\-r\fR option may be used to copy them to the local machine and back
again after signing. If the files live on the \fBlocal\fR machine, then
they may be transferred to the remote machine for signing using
\fBdebrsign\fR(1). However note that it is probably safer to have your
trusted signing machine use \fBdebsign\fR to connect to the untrusted
non-signing machine, rather than using \fBdebrsign\fR to make the
connection in the reverse direction.
.PP
This program can take default settings from the \fBdevscripts\fR
configuration files, as described below.
.SH OPTIONS
.TP
.B \-r \fR[\fIusername\fB@\fR]\fIremotehost\fR
The files to be signed live on the specified remote host. In this case,
a \fI.dsc\fR, \fI.buildinfo\fR or \fI.changes\fR file must be explicitly
named, with an absolute directory or one relative to the remote home
directory. \fBscp\fR will be used for the copying. The
\fR[\fIusername\fB@\fR]\fIremotehost\fB:\fIfilename\fR syntax is
permitted as an alternative. Wildcards (\fB*\fR etc.) are allowed.
.TP
.B \-p\fIprogname\fR
When \fBdebsign\fR needs to execute GPG to sign it will run \fIprogname\fR
(searching the \fBPATH\fR if necessary), instead of \fBgpg\fR.
.TP
.B \-m\fImaintainer\fR
Specify the maintainer name to be used for signing. (See
\fBdpkg-buildpackage\fR(1) for more information about the differences
between \fB\-m\fR, \fB\-e\fR and \fB\-k\fR when building packages;
\fBdebsign\fR makes no use of these distinctions except with respect
to the precedence of the various options. These multiple options are
provided so that the program will behave as expected when called by
\fBdebuild\fR(1).)
.TP
.B \-e\fImaintainer\fR
Same as \fB\-m\fR but takes precedence over it.
.TP
.B \-k\fIkeyid\fR
Specify the key ID to be used for signing; overrides any \fB\-m\fR
and \fB\-e\fR options.
.TP
\fB\-S\fR
Look for a source-only \fI.changes\fR file instead of a binary-build
\fI.changes\fR file.
.TP
\fB\-a\fIdebian-architecture\fR, \fB\-t\fIGNU-system-type\fR
See \fBdpkg-architecture\fR(1) for a description of these options.
They affect the search for the \fI.changes\fR file. They are provided
to mimic the behaviour of \fBdpkg-buildpackage\fR when determining the
name of the \fI.changes\fR file.
.TP
\fB\-\-multi\fR
Multiarch \fI.changes\fR mode: This signifies that \fBdebsign\fR should
use the most recent file with the name pattern
\fIpackage_version_*+*.changes\fR as the \fI.changes\fR file, allowing for the
\fI.changes\fR files produced by \fBdpkg-cross\fR.
.TP
\fB\-\-re\-sign\fR, \fB\-\-no\-re\-sign\fR
Recreate signature, respectively use the existing signature, if the
file has been signed already. If neither option is given and an already
signed file is found the user is asked if he or she likes to use the
current signature.
.TP
\fB\-\-debs\-dir\fR \fIDIR\fR
Look for the files to be signed in directory \fIDIR\fR instead of the
parent of the source directory. This should either be an absolute path
or relative to the top of the source directory.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
.BR \-\-help ", " \-h
Display a help message and exit successfully.
.TP
.B \-\-version
Display version and copyright information and exit successfully.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variables are:
.TP
.B DEBSIGN_PROGRAM
Setting this is equivalent to giving a \fB\-p\fR option.
.TP
.B DEBSIGN_MAINT
This is the \fB\-m\fR option.
.TP
.B DEBSIGN_KEYID
And this is the \fB\-k\fR option.
.TP
.B DEBSIGN_ALWAYS_RESIGN
Always re-sign files even if they are already signed, without prompting.
.TP
.B DEBRELEASE_DEBS_DIR
This specifies the directory in which to look for the files to be
signed, and is either an absolute path or relative to the top of the
source tree. This corresponds to the \fB\-\-debs\-dir\fR command line
option. This directive could be used, for example, if you always use
\fBpbuilder\fR or \fBsvn-buildpackage\fR to build your packages. Note
that it also affects \fBdebrelease\fR(1) in the same way, hence the
strange name of the option.
.SH "SEE ALSO"
.BR debrsign (1),
.BR debuild (1),
.BR dpkg-architecture (1),
.BR dpkg-buildpackage (1),
.BR gpg (1),
.BR md5sum (1),
.BR sha1sum (1),
.BR sha256sum (1),
.BR scp (1),
.BR devscripts.conf (5)
.SH AUTHOR
This program was written by Julian Gilbey <jdg@debian.org> and is
copyright under the GPL, version 2 or later.

View file

@ -0,0 +1,99 @@
# /usr/share/bash-completion/completions/debsign
# Bash command completion for debsign(1).
# Documentation: bash(1), section “Programmable Completion”.
shopt -s progcomp
_have _debsign_completion &&
_debsign_completion () {
COMPREPLY=()
local cur="${COMP_WORDS[COMP_CWORD]}"
local prev="${COMP_WORDS[COMP_CWORD-1]}"
local options=(
-h --help --version
-r -m -e -k
-a -t --multi
-p --debs-dir
-S
--re-sign --no-re-sign
--no-conf --noconf
)
case "$prev" in
-r)
# The option requires a non-option argument here, but we
# have no feasible way to generate auto-completion matches
# for username@remotehost. Use an empty set.
local host_options=""
COMPREPLY=( $(compgen -W "$host_options" -- "$cur") )
;;
-m|-e)
# The previous option requires an argument, but we
# have no feasible way to generate auto-completion matches
# for a maintainer identifier. Use an empty set.
local maintainer_options=""
COMPREPLY=( $(compgen -W "$maintainer_options" -- "$cur") )
;;
-k)
# Provide completions for OpenPGP secret key IDs.
local keyid_options=$(
gpg --fixed-list-mode --with-colons --fingerprint \
--list-secret-keys \
| awk -F':' '/^sec/{print $5}' )
COMPREPLY=( $(
compgen -W "$keyid_options" | grep "^${cur:-.}"
) )
;;
-a)
# Provide completions for system architecture identifiers.
local arch_options=$(dpkg-architecture --list-known)
COMPREPLY=( $(compgen -W "$arch_options" -- "$cur") )
;;
-t)
# The previous option requires an argument, but we
# have no feasible way to generate auto-completion matches
# for a GNU system type identifier. Use an empty set.
local type_options=""
COMPREPLY=( $(compgen -W "$type_options" -- "$cur") )
;;
-p)
# Provide completions for available commands.
COMPREPLY=( $(compgen -A command -- "$cur") )
;;
--debs-dir)
# Provide completions for existing directory paths.
COMPREPLY=( $(compgen -o dirnames -A directory -- "$cur") )
;;
*)
COMPREPLY=( $(
compgen -G "${cur}*.changes"
compgen -G "${cur}*.buildinfo"
compgen -G "${cur}*.dsc"
compgen -G "${cur}*.commands"
compgen -W "${options[*]}" -- "$cur"
) )
compopt -o filenames
compopt -o plusdirs
;;
esac
return 0
} && complete -F _debsign_completion debsign
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

853
scripts/debsign.sh Executable file
View file

@ -0,0 +1,853 @@
#!/bin/sh
# This program is designed to GPG sign .dsc, .buildinfo, or .changes
# files (or any combination of these) in the form needed for a legal
# Debian upload. It is based in part on dpkg-buildpackage.
# Debian GNU/Linux debsign. Copyright (C) 1999 Julian Gilbey.
# Modifications to work with GPG by Joseph Carter and Julian Gilbey
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# Abort if anything goes wrong
set -e
PROGNAME=${0##*/}
PRECIOUS_FILES=0
MODIFIED_CONF_MSG='Default settings modified by devscripts configuration files:'
HAVE_SIGNED=""
NUM_SIGNED=0
# Temporary directories
signingdir=""
remotefilesdir=""
trap cleanup_tmpdir EXIT
# --- Functions
mksigningdir() {
if [ -z "$signingdir" ]; then
signingdir="$(mktemp -dt debsign.XXXXXXXX)" || {
echo "$PROGNAME: Can't create temporary directory" >&2
echo "Aborting..." >&2
exit 1
}
fi
}
mkremotefilesdir() {
if [ -z "$remotefilesdir" ]; then
remotefilesdir="$(mktemp -dt debsign.XXXXXXXX)" || {
echo "$PROGNAME: Can't create temporary directory" >&2
echo "Aborting..." >&2
exit 1
}
fi
}
usage() {
echo \
"Usage: debsign [options] [changes, buildinfo, dsc or commands file]
Options:
-r [username@]remotehost
The machine on which the files live. If given, then a
changes file with full pathname (or relative to the
remote home directory) must be given as the main
argument in the rest of the command line.
-k<keyid> The key to use for signing
-p<sign-command> The command to use for signing
-e<maintainer> Sign using key of <maintainer> (takes precedence over -m)
-m<maintainer> The same as -e
-S Use changes file made for source-only upload
-a<arch> Use changes file made for Debian target architecture <arch>
-t<target> Use changes file made for GNU target architecture <target>
--multi Use most recent multiarch .changes file found
--re-sign Re-sign if the file is already signed.
--no-re-sign Don't re-sign if the file is already signed.
--debs-dir <directory>
The location of the files to be signed when called from
within a source tree (default "..")
--no-conf, --noconf
Don't read devscripts config files;
must be the first option given
--help Show this message
--version Show version and copyright information
If an explicit filename is specified, it along with any child .buildinfo and
.dsc files are signed. Otherwise, debian/changelog is parsed to find the
changes file.
$MODIFIED_CONF_MSG"
}
version() {
echo \
"This is debsign, from the Debian devscripts package, version ###VERSION###
This code is copyright 1999 by Julian Gilbey, all rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later."
}
temp_filename() {
local filename
if ! [ -w "$(dirname "$1")" ]; then
filename=$(mktemp -t "$(basename "$1").$2.XXXXXXXXXX") || {
echo "$PROGNAME: Unable to create temporary file; aborting" >&2
exit 1
}
else
filename="$1.$2"
fi
echo "$filename"
}
to_bool() {
if "$@"; then echo true; else echo false; fi
}
movefile() {
if [ -w "$(dirname "$2")" ]; then
mv -f -- "$1" "$2"
else
cat "$1" > "$2"
rm -f "$1"
fi
}
cleanup_tmpdir() {
if [ -n "$remotefilesdir" ] && [ -d "$remotefilesdir" ]; then
if [ "$PRECIOUS_FILES" -gt 0 ]; then
echo "$PROGNAME: aborting with $PRECIOUS_FILES signed files in $remotefilesdir" >&2
# Only produce the warning once...
PRECIOUS_FILES=0
else
cd ..
rm -rf "$remotefilesdir"
fi
fi
if [ -n "$signingdir" ] && [ -d "$signingdir" ]; then
rm -rf "$signingdir"
fi
}
mustsetvar() {
if [ "x$2" = x ]
then
echo >&2 "$PROGNAME: unable to determine $3"
exit 1
else
# echo "$PROGNAME: $3 is $2"
eval "$1=\"\$2\""
fi
}
# This takes two arguments: the name of the file to sign and the
# key or maintainer name to use. NOTE: this usage differs from that
# of dpkg-buildpackage, because we do not know all of the necessary
# information when this function is read first.
signfile() {
local type="$1"
local file="$2"
local signas="$3"
local savestty=$(stty -g 2>/dev/null) || true
mksigningdir
UNSIGNED_FILE="$signingdir/$(basename "$file")"
ASCII_SIGNED_FILE="${UNSIGNED_FILE}.asc"
(cat "$file" ; echo "") > "$UNSIGNED_FILE"
$signcommand --no-auto-check-trustdb \
--local-user "$signas" --clearsign \
--openpgp \
--personal-digest-preferences 'SHA512 SHA384 SHA256 SHA224' \
--list-options no-show-policy-urls \
--armor --textmode --output "$ASCII_SIGNED_FILE"\
"$UNSIGNED_FILE" || \
{ SAVESTAT=$?
echo "$PROGNAME: $signcommand error occurred! Aborting...." >&2
stty $savestty 2>/dev/null || true
exit $SAVESTAT
}
stty $savestty 2>/dev/null || true
echo
PRECIOUS_FILES=$(($PRECIOUS_FILES + 1))
HAVE_SIGNED="${HAVE_SIGNED:+${HAVE_SIGNED}, }$type"
NUM_SIGNED=$((NUM_SIGNED + 1))
movefile "$ASCII_SIGNED_FILE" "$file"
}
withecho() {
echo " $@"
"$@"
}
file_is_already_signed() {
test "$(head -n 1 "$1")" = "-----BEGIN PGP SIGNED MESSAGE-----"
}
unsignfile() {
UNSIGNED_FILE="$(temp_filename "$1" "unsigned")"
sed -e '1,/^$/d; /^$/,$d' "$1" > "$UNSIGNED_FILE"
movefile "$UNSIGNED_FILE" "$1"
}
# Has the dsc file already been signed, perhaps from a previous, partially
# successful invocation of debsign? We give the user the option of
# resigning the file or accepting it as is. Returns success if already
# and failure if the file needs signing. Parameters: $1=filename,
# $2=file type for message (e.g. "changes", "commands")
check_already_signed() {
file_is_already_signed "$1" || return 1
local resign
if [ "$opt_re_sign" = "true" ]; then
resign="true"
elif [ "$opt_re_sign" = "false" ]; then
resign="false"
else
response=n
if [ -z "$DEBSIGN_ALWAYS_RESIGN" ]; then
printf "The .$2 file is already signed.\nWould you like to use the current signature? [Yn]"
read response
fi
case $response in
[Nn]*) resign="true" ;;
*) resign="false" ;;
esac
fi
[ "$resign" = "true" ] || \
return 0
withecho unsignfile "$1"
return 1
}
# --- main script
# Unset GREP_OPTIONS for sanity
unset GREP_OPTIONS
# Boilerplate: set config variables
DEFAULT_DEBSIGN_ALWAYS_RESIGN=
DEFAULT_DEBSIGN_PROGRAM=
DEFAULT_DEBSIGN_MAINT=
DEFAULT_DEBSIGN_KEYID=
DEFAULT_DEBRELEASE_DEBS_DIR=..
VARS="DEBSIGN_ALWAYS_RESIGN DEBSIGN_PROGRAM DEBSIGN_MAINT"
VARS="$VARS DEBSIGN_KEYID DEBRELEASE_DEBS_DIR"
if [ "$1" = "--no-conf" -o "$1" = "--noconf" ]; then
shift
MODIFIED_CONF_MSG="$MODIFIED_CONF_MSG
(no configuration files read)"
# set defaults
for var in $VARS; do
eval "$var=\$DEFAULT_$var"
done
else
# Run in a subshell for protection against accidental errors
# in the config files
eval $(
set +e
for var in $VARS; do
eval "$var=\$DEFAULT_$var"
done
for file in /etc/devscripts.conf ~/.devscripts
do
[ -r $file ] && . $file
done
set | grep -E '^(DEBSIGN|DEBRELEASE|DEVSCRIPTS)_')
# We do not replace this with a default directory to avoid accidentally
# signing a broken package
DEBRELEASE_DEBS_DIR="$(echo "${DEBRELEASE_DEBS_DIR%/}" | sed -e 's%/\+%/%g')"
# set config message
MODIFIED_CONF=''
for var in $VARS; do
eval "if [ \"\$$var\" != \"\$DEFAULT_$var\" ]; then
MODIFIED_CONF_MSG=\"\$MODIFIED_CONF_MSG
$var=\$$var\";
MODIFIED_CONF=yes;
fi"
done
if [ -z "$MODIFIED_CONF" ]; then
MODIFIED_CONF_MSG="$MODIFIED_CONF_MSG
(none)"
fi
fi
maint="$DEBSIGN_MAINT"
signkey="$DEBSIGN_KEYID"
debsdir="$DEBRELEASE_DEBS_DIR"
debsdir_warning="config file specified DEBRELEASE_DEBS_DIR directory $DEBRELEASE_DEBS_DIR does not exist!"
signcommand=''
if [ -n "$DEBSIGN_PROGRAM" ]; then
signcommand="$DEBSIGN_PROGRAM"
else
if command -v gpg > /dev/null; then
signcommand=gpg
fi
fi
TEMP=$(getopt -n "$PROGNAME" -o 'p:m:e:k:Sa:t:r:h' \
-l 'multi,re-sign,no-re-sign,debs-dir:' \
-l 'noconf,no-conf,help,version' \
-- "$@") || (rc=$?; usage >&2; exit $rc)
eval set -- "$TEMP"
while true
do
case "$1" in
-p) signcommand="$2"; shift ;;
-m) maint="$2"; shift ;;
-e) maint="$2"; shift ;;
-k) signkey="$2"; shift ;;
-S) sourceonly="true" ;;
-a) targetarch="$2"; shift ;;
-t) targetgnusystem="$2"; shift ;;
--multi) multiarch="true" ;;
--re-sign) opt_re_sign="true" ;;
--no-re-sign) opt_re_sign="false" ;;
-r) remotehost=$2; shift
# Allow for the [user@]host:filename format
hostpart="${remotehost%:*}"
filepart="${remotehost#*:}"
if [ -n "$filepart" -a "$filepart" != "$remotehost" ]; then
remotehost="$hostpart"
set -- "$@" "$filepart"
fi
;;
--debs-dir)
shift
opt_debsdir="$(echo "${1%/}" | sed -e 's%/\+%/%g')"
debsdir_warning="could not find directory $opt_debsdir!"
;;
--no-conf|--noconf)
echo "$PROGNAME: $1 is only acceptable as the first command-line option!" >&2
exit 1 ;;
-h|--help)
usage; exit 0 ;;
--version)
version; exit 0 ;;
--) shift; break ;;
esac
shift
done
debsdir=${opt_debsdir:-$debsdir}
if [ -z "$signcommand" ]; then
echo "Could not find a signing program!" >&2
exit 1
fi
if echo "${signkey}" | grep -E -qs '^(0x)?[a-zA-Z0-9]{8}$'; then
echo "Refusing to sign with short key ID '$signkey'!" >&2
exit 1
fi
if echo "${signkey}" | grep -E -qs '^(0x)?[a-zA-Z0-9]{16}$'; then
echo "long key IDs are discouraged; please use key fingerprints instead" >&2
fi
ensure_local_copy() {
local remotehost="$1"
local remotefile="$2"
local file="$3"
local type="$4"
if [ -n "$remotehost" ]
then
if [ ! -f "$file" ]
then
withecho scp "$remotehost:$remotefile" "$file"
fi
fi
if [ ! -f "$file" -o ! -r "$file" ]
then
echo "$PROGNAME: Can't find or can't read $type file $file!" >&2
exit 1
fi
}
fixup_control() {
local filter_out="$1"
local childtype="$2"
local parenttype="$3"
local child="$4"
local parent="$5"
test -r "$child" || {
echo "$PROGNAME: Can't read .$childtype file $child!" >&2
return 1
}
local md5=$(md5sum "$child" | cut -d' ' -f1)
local sha1=$(sha1sum "$child" | cut -d' ' -f1)
local sha256=$(sha256sum "$child" | cut -d' ' -f1)
perl -i -pe 'BEGIN {
'" \$file='$child'; \$md5='$md5'; "'
'" \$sha1='$sha1'; \$sha256='$sha256'; "'
$size=(-s $file); ($base=$file) =~ s|.*/||;
$infiles=0; $inmd5=0; $insha1=0; $insha256=0; $format="";
}
if(/^Format:\s+(.*)/) {
$format=$1;
die "Unrecognised .$parenttype format: $format\n"
unless $format =~ /^\d+(\.\d+)*$/;
($major, $minor) = split(/\./, $format);
$major+=0;$minor+=0;
die "Unsupported .$parenttype format: $format\n"
if('"$filter_out"');
}
/^Files:/i && ($infiles=1,$inmd5=0,$insha1=0,$insha256=0);
if(/^Checksums-Sha1:/i) {$insha1=1;$infiles=0;$inmd5=0;$insha256=0;}
elsif(/^Checksums-Sha256:/i) {
$insha256=1;$infiles=0;$inmd5=0;$insha1=0;
} elsif(/^Checksums-Md5:/i) {
$inmd5=1;$infiles=0;$insha1=0;$insha256=0;
} elsif(/^Checksums-.*?:/i) {
die "Unknown checksum format: $_\n";
}
/^\s*$/ && ($infiles=0,$inmd5=0,$insha1=0,$insha256=0);
if ($infiles &&
/^ (\S+) (\d+) (\S+) (\S+) \Q$base\E\s*$/) {
$_ = " $md5 $size $3 $4 $base\n";
$infiles=0;
}
if ($inmd5 &&
/^ (\S+) (\d+) \Q$base\E\s*$/) {
$_ = " $md5 $size $base\n";
$inmd5=0;
}
if ($insha1 &&
/^ (\S+) (\d+) \Q$base\E\s*$/) {
$_ = " $sha1 $size $base\n";
$insha1=0;
}
if ($insha256 &&
/^ (\S+) (\d+) \Q$base\E\s*$/) {
$_ = " $sha256 $size $base\n";
$insha256=0;
}' "$parent"
}
fixup_buildinfo() {
fixup_control '($major != 0 or $minor > 2) and ($major != 1 or $minor > 0)' dsc buildinfo "$@"
}
fixup_changes() {
local childtype="$1"
shift
fixup_control '$major!=1 or $minor > 8 or $minor < 7' $childtype changes "$@"
}
withtempfile() {
local filetype="$1"
local mainfile="$2"
shift 2
local temp_file="$(temp_filename "$mainfile" "temp")"
cp "$mainfile" "$temp_file"
if "$@" "$temp_file"; then
if ! cmp -s "$mainfile" "$temp_file"; then
# emulate output of "withecho" but on the mainfile
echo " $@" "$mainfile" >&2
fi
movefile "$temp_file" "$mainfile"
else
rm "$temp_file"
echo "$PROGNAME: Error processing .$filetype file (see above)" >&2
exit 1
fi
}
guess_signas() {
if [ -n "$maint" ]
then maintainer="$maint"
# Try the new "Changed-By:" field first
else maintainer=$(sed -n 's/^Changed-By: //p' $1)
fi
if [ -z "$maintainer" ]
then maintainer=$(sed -n 's/^Maintainer: //p' $1)
fi
echo "${signkey:-$maintainer}"
}
maybesign_dsc() {
local signas="$1"
local remotehost="$2"
local dsc="$3"
if check_already_signed "$dsc" dsc; then
echo "Leaving current signature unchanged." >&2
return
fi
withecho signfile dsc "$dsc" "$signas"
if [ -n "$remotehost" ]
then
withecho scp "$dsc" "$remotehost:$remotedir"
PRECIOUS_FILES=$(($PRECIOUS_FILES - 1))
fi
}
maybesign_buildinfo() {
local signas="$1"
local remotehost="$2"
local buildinfo="$3"
local dsc="$4"
if check_already_signed "$buildinfo" "buildinfo"; then
echo "Leaving current signature unchanged." >&2
return
fi
if [ -n "$dsc" ]; then
maybesign_dsc "$signas" "$remotehost" "$dsc"
withtempfile buildinfo "$buildinfo" fixup_buildinfo "$dsc"
fi
withecho signfile buildinfo "$buildinfo" "$signas"
if [ -n "$remotehost" ]
then
withecho scp "$buildinfo" "$remotehost:$remotedir"
PRECIOUS_FILES=$(($PRECIOUS_FILES - 1))
fi
}
maybesign_changes() {
local signas="$1"
local remotehost="$2"
local changes="$3"
local buildinfo="$4"
local dsc="$5"
if check_already_signed "$changes" "changes"; then
echo "Leaving current signature unchanged." >&2
return
fi
hasdsc="$(to_bool [ -n "$dsc" ])"
hasbuildinfo="$(to_bool [ -n "$buildinfo" ])"
if $hasbuildinfo; then
# assume that this will also sign the same dsc if it's available
maybesign_buildinfo "$signas" "$remotehost" "$buildinfo" "$dsc"
elif $hasdsc; then
maybesign_dsc "$signas" "$remotehost" "$dsc"
fi
if $hasdsc; then
withtempfile changes "$changes" fixup_changes dsc "$dsc"
fi
if $hasbuildinfo; then
withtempfile changes "$changes" fixup_changes buildinfo "$buildinfo"
fi
withecho signfile changes "$changes" "$signas"
if [ -n "$remotehost" ]
then
withecho scp "$changes" "$remotehost:$remotedir"
PRECIOUS_FILES=$(($PRECIOUS_FILES - 1))
fi
}
report_signed() {
if [ $NUM_SIGNED -eq 1 ]; then
echo "Successfully signed $HAVE_SIGNED file"
elif [ $NUM_SIGNED -gt 0 ]; then
echo "Successfully signed $HAVE_SIGNED files"
fi
}
dosigning() {
# Do we have to download the changes file?
if [ -n "$remotehost" ]
then
mkremotefilesdir
cd "$remotefilesdir"
remotechanges=$changes
remotebuildinfo=$buildinfo
remotedsc=$dsc
remotecommands=$commands
changes=$(basename "$changes")
buildinfo=$(basename "$buildinfo")
dsc=$(basename "$dsc")
commands=$(basename "$commands")
if [ -n "$changes" ]; then
if [ ! -f "$changes" ]; then
# Special handling for changes to support supplying a glob
# and downloading all matching changes files (c.f., #491627)
withecho scp "$remotehost:$remotechanges" .
fi
fi
if [ -n "$changes" ] && echo "$changes" | grep -qE '[][*?]'
then
for changes in $changes
do
dsc=
buildinfo=
printf "\n"
dosigning;
done
exit 0;
fi
fi
if [ -n "$commands" ] # sign .commands file
then
ensure_local_copy "$remotehost" "$remotecommands" "$commands" commands
check_already_signed "$commands" commands && {
echo "Leaving current signature unchanged." >&2
return
}
# simple validator for .commands files, see
# ftp://ftp.upload.debian.org/pub/UploadQueue/README
perl -ne 'BEGIN { $uploader = 0; $incommands = 0; }
END { exit $? if $?;
if ($uploader && $incommands) { exit 0; }
else { die ".commands file missing Uploader or Commands field\n"; }
}
sub checkcommands {
chomp($line=$_[0]);
if ($line =~ m%^\s*reschedule\s+[^\s/]+\.changes\s+[0-9]+-day\s*$%) { return 0; }
if ($line =~ m%^\s*cancel\s+[^\s/]+\.changes\s*$%) { return 0; }
if ($line =~ m%^\s*rm(\s+(?:DELAYED/[0-9]+-day/)?[^\s/]+)+\s*$%) { return 0; }
if ($line eq "") { return 0; }
die ".commands file has invalid Commands line: $line\n";
}
if (/^Uploader:/) {
if ($uploader) { die ".commands file has too many Uploader fields!\n"; }
$uploader++;
} elsif (! $incommands && s/^Commands:\s*//) {
$incommands=1; checkcommands($_);
} elsif ($incommands == 1) {
if (s/^\s+//) { checkcommands($_); }
elsif (/./) { die ".commands file: extra stuff after Commands field!\n"; }
else { $incommands = 2; }
} else {
next if /^\s*$/;
if (/./) { die ".commands file: extra stuff after Commands field!\n"; }
}' $commands || {
echo "$PROGNAME: .commands file appears to be invalid. see:
ftp://ftp.upload.debian.org/pub/UploadQueue/README
for valid format" >&2;
exit 1; }
if [ -n "$maint" ]
then maintainer="$maint"
else
maintainer=$(sed -n 's/^Uploader: //p' $commands)
if [ -z "$maintainer" ]
then
echo "Unable to parse Uploader, .commands file invalid."
exit 1
fi
fi
signas="${signkey:-$maintainer}"
withecho signfile commands "$commands" "$signas"
if [ -n "$remotehost" ]
then
withecho scp "$commands" "$remotehost:$remotedir"
PRECIOUS_FILES=$(($PRECIOUS_FILES - 1))
fi
report_signed
elif [ -n "$changes" ]
then
ensure_local_copy "$remotehost" "$remotechanges" "$changes" changes
derive_childfile "$changes" dsc
if [ -n "$dsc" ]
then
ensure_local_copy "$remotehost" "${remotedir}$dsc" "$dsc" dsc
fi
derive_childfile "$changes" buildinfo
if [ -n "$buildinfo" ]
then
ensure_local_copy "$remotehost" "${remotedir}$buildinfo" "$buildinfo" buildinfo
fi
signas="$(guess_signas "$changes")"
maybesign_changes "$signas" "$remotehost" \
"$changes" "$buildinfo" "$dsc"
report_signed
elif [ -n "$buildinfo" ]
then
ensure_local_copy "$remotehost" "$remotebuildinfo" "$buildinfo" buildinfo
derive_childfile "$buildinfo" dsc
if [ -n "$dsc" ]
then
ensure_local_copy "$remotehost" "${remotedir}$dsc" "$dsc" dsc
fi
signas="$(guess_signas "$buildinfo")"
maybesign_buildinfo "$signas" "$remotehost" \
"$buildinfo" "$dsc"
report_signed
else
ensure_local_copy "$remotehost" "$remotedsc" "$dsc" dsc
signas="$(guess_signas "$dsc")"
maybesign_dsc "$signas" "$remotehost" "$dsc"
report_signed
fi
}
derive_childfile() {
local base="$1"
local ext="$2"
local fname dir
fname="$(sed -n '/^\(Checksum\|Files\)/,/^\(Checksum\|Files\)/s/.*[ ]\([^ ]*\.'"$ext"'\)$/\1/p' "$base" | head -n1)"
if [ -n "$fname" ]
then
get_dirname "$base" dir
eval "$ext=\"${dir}$fname\""
else
eval "$ext="
fi
}
get_dirname() {
local path="$1"
local varname="$2"
local d
d="$(dirname "$path")"
if [ "$d" = "." ]
then
d=""
else
d="$d/"
fi
eval "$varname=\"$d\""
}
# If there is a command-line parameter, it is the name of a .changes file
# If not, we must be at the top level of a source tree and will figure
# out its name from debian/changelog
case $# in
0) # We have to parse debian/changelog to find the current version
# check sanity of debsdir
if ! [ -d "$debsdir" ]; then
echo "$PROGNAME: $debsdir_warning" >&2
exit 1
fi
if [ -n "$remotehost" ]; then
echo "$PROGNAME: Need to specify a remote file location when giving -r!" >&2
exit 1
fi
if [ ! -r debian/changelog ]; then
echo "$PROGNAME: Must be run from top of source dir or a .changes file given as arg" >&2
exit 1
fi
mustsetvar package "`dpkg-parsechangelog -SSource`" "source package"
mustsetvar version "`dpkg-parsechangelog -SVersion`" "source version"
if [ "x$sourceonly" = x ]
then
if [ -n "$targetarch" ] && [ -n "$targetgnusystem" ]; then
mustsetvar arch "$(dpkg-architecture "-a${targetarch}" "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture"
elif [ -n "$targetarch" ]; then
mustsetvar arch "$(dpkg-architecture "-a${targetarch}" -qDEB_HOST_ARCH)" "build architecture"
elif [ -n "$targetgnusystem" ]; then
mustsetvar arch "$(dpkg-architecture "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture"
else
mustsetvar arch "$(dpkg-architecture -qDEB_HOST_ARCH)" "build architecture"
fi
else
arch=source
fi
sversion=$(echo "$version" | perl -pe 's/^\d+://')
pva="${package}_${sversion}_${arch}"
changes="$debsdir/$pva.changes"
if [ -n "$multiarch" -o ! -r $changes ]; then
changes=$(ls "$debsdir/${package}_${sversion}_*+*.changes" "$debsdir/${package}_${sversion}_multi.changes" 2>/dev/null | head -1)
# TODO: dpkg-cross does not yet do buildinfo, so don't worry about it here
if [ -z "$multiarch" ]; then
if [ -n "$changes" ]; then
echo "$PROGNAME: could not find normal .changes file but found multiarch file:" >&2
echo " $changes" >&2
echo "Using this changes file instead." >&2
else
echo "$PROGNAME: Can't find or can't read changes file $changes!" >&2
exit 1
fi
elif [ -n "$multiarch" -a -z "$changes" ]; then
echo "$PROGNAME: could not find any multiarch .changes file with name" >&2
echo "$debsdir/${package}_${sversion}_*.changes" >&2
exit 1
fi
fi
derive_childfile "$changes" dsc
derive_childfile "$changes" buildinfo
dosigning;
;;
*) while [ $# -gt 0 ]; do
changes=
buildinfo=
dsc=
commands=
case "$1" in
*.dsc)
dsc=$1
;;
*.buildinfo)
buildinfo=$1
;;
*.changes)
changes=$1
;;
*.commands)
commands=$1
;;
*)
echo "$PROGNAME: Only a .changes, .buildinfo, .dsc or .commands file is allowed as argument!" >&2
exit 1 ;;
esac
get_dirname "$1" remotedir
dosigning
shift
done
;;
esac
exit 0

160
scripts/debsnap.1 Normal file
View file

@ -0,0 +1,160 @@
.\" for manpage-specific macros, see man(7)
.TH DEBSNAP 1 "July 3, 2010" "Debian devscripts" "DebSnap User Manual"
.SH NAME
debsnap \- retrieve old snapshots of Debian packages
.SH SYNOPSIS
.B debsnap
.RI [ options ] " package " [ version ]
.B debsnap
.RB [ -h " | " \-\-help ] " " [ \-\-version ]
.SH DESCRIPTION
\fBdebsnap\fP is a tool to help with retrieving snapshots of old packages from
a daily archive repository.
The only publicly available snapshot archive is currently located at
\fIhttps://snapshot.debian.org\fP
By default, debsnap will download all the available versions for \fIpackage\fP
that are found in the snapshot archive. If a \fIversion\fP is specified, only
that particular version will be downloaded, if available.
.SH OPTIONS
The following options are available:
.TP
.BI -d " destination\fR,\fP " \-\-destdir " destination"
Directory to place retrieved packages.
.TP
.BR \-f ", " \-\-force
Force writing into an existing \fIdestination\fP. By default \fBdebsnap\fP will
insist the destination directory does not exist yet unless it is explicitly
specified to be '\fB.\fR' (the current working directory). This is to avoid files
being accidentally overwritten by what is fetched from the archive and to
provide a guarantee for other scripts that only the files fetched will be
present there upon completion.
.TP
.BR \-v ", " \-\-verbose
Report on the \fBdebsnap\fP configuration being used and progress during the
download operation. Please always use this option when reporting bugs.
.TP
.BR \-l ", " \-\-list
Don't download but just list versions.
.TP
.BR \-\-binary
Download binary packages instead of source packages.
.TP
.BR \-a ", " \-\-architecture
Specify architecture of downloaded binary packages. Implies \fB\-\-binary\fP.
This can be given multiple times in order to download binary packages for
multiple architectures.
.TP
.B \-\-first
Specify the minimum version of a package which will be downloaded. Any
versions which compare larger than this, according to \fBdpkg\fP, will be
considered for download. May be used in combination with \fB\-\-last\fP.
.TP
.B \-\-last
Specify the maximum version of a package which will be downloaded. Any package
versions which compare less than this, according to \fBdpkg\fP, will be
considered for download. May be used in combination with \fB\-\-first\fP.
.TP
.BR \-h ", " \-\-help
Show a summary of these options.
.TP
.B \-\-version
Show the version of \fBdebsnap\fP.
.SH CONFIGURATION OPTIONS
\fBdebsnap\fP may also be configured through the use of the following options
in the devscripts configuration files:
.TP
.B DEBSNAP_VERBOSE
Same as the command line option \fB\-\-verbose\fP. Set to \fIyes\fP to enable.
.TP
.B DEBSNAP_DESTDIR
Set a default path for the destination directory. If unset
\fI./source\-<package_name>\fP will be used. The command line option
\fB\-\-destdir\fP will override this.
.TP
.B DEBSNAP_BASE_URL
The base url for the snapshots archive.
If unset this defaults to \fIhttps://snapshot.debian.org\fP
.SH EXIT STATUS
\fBdebsnap\fP will return an exit status of 0 if all operations succeeded,
1 if a fatal error occurred, and 2 if some packages failed to be downloaded
but operations otherwise succeeded as expected. In some cases packages may
fail to be downloaded because they are no longer available on the snapshot
mirror, so any caller should expect this may occur in normal use.
.SH EXAMPLES
.TP
.BR "debsnap -a amd64 xterm 256-1"
Download the binary package of a specific xterm version for amd64 architecture.
.TP
.BR "debsnap -a armel xterm"
Download binary packages for all versions of xterm for armel architecture.
.TP
.BR "debsnap --binary xterm 256-1"
Download binary packages for a specific xterm version but for all architectures.
.TP
.BR "debsnap --binary xterm"
Download binary packages for all versions of xterm for all architectures.
.TP
.BR "debsnap -v --first 347-1 --last 348-2 xterm"
Download source packages for local architecture of xterm, between 347-1 and
348-2 revisions, inclusive, showing the progress when doing it.
.TP
.BR "aptitude search '~i' -F '%p %V' | while read pkg ver; do debsnap -a $(dpkg-architecture -qDEB_HOST_ARCH) -a all $pkg $ver; done"
Download binary packages of all packages that are installed on the system.
.SH FILES
.TP
.I /etc/devscripts.conf
Global devscripts configuration options. Will override hardcoded defaults.
.TP
.I ~/.devscripts
Per\-user configuration options. Will override any global configuration.
.SH SEE ALSO
.BR devscripts (1),
.BR devscripts.conf (5),
.BR git-debimport (1)
.SH AUTHORS
David Paleino <dapal@debian.org>
.SH COPYRIGHT
Copyright \(co 2010 David Paleino
Permission is granted to copy, distribute and/or modify this document under
the terms of the GNU General Public License, Version 3 or (at your option)
any later version published by the Free Software Foundation.
On Debian systems, the complete text of the GNU General Public License can
be found in \fI/usr/share/common\-licenses/GPL\fP.
.SH BUGS
.SS Reporting bugs
The program is part of the devscripts package. Please report bugs using
`\fBreportbug devscripts\fP`

434
scripts/debsnap.pl Executable file
View file

@ -0,0 +1,434 @@
#!/usr/bin/perl
# vim: set ai shiftwidth=4 tabstop=4 expandtab:
# Copyright © 2010, David Paleino <d.paleino@gmail.com>,
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use strict;
use warnings;
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use File::Basename;
use Cwd qw/cwd abs_path/;
use File::Path qw/make_path/;
use Dpkg::Version;
use JSON::PP;
my $progname = basename($0);
eval {
require LWP::Simple;
require LWP::UserAgent;
no warnings;
$LWP::Simple::ua = LWP::UserAgent->new(
agent => 'LWP::UserAgent/Devscripts/###VERSION###');
$LWP::Simple::ua->env_proxy();
};
if ($@) {
if ($@ =~ m/Can\'t locate LWP/) {
die
"$progname: Unable to run: the libwww-perl package is not installed";
} else {
die "$progname: Unable to run: Couldn't load LWP::Simple: $@";
}
}
my $modified_conf_msg = '';
my %config_vars = ();
my %opt = (architecture => []);
my $package = '';
my $pkgversion;
my $firstversion;
my $lastversion;
my $warnings = 0;
sub fatal($);
sub verbose($);
sub version {
print <<"EOF";
This is $progname, from the Debian devscripts package, version ###VERSION###
This code is copyright 2010 by David Paleino <dapal\@debian.org>.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the GNU
General Public License v3 or, at your option, any later version.
EOF
exit 0;
}
sub usage {
my $rc = shift;
print <<"EOF";
$progname [options] <package name> [package version]
Automatically downloads packages from snapshot.debian.org
The following options are supported:
-h, --help Shows this help message
--version Shows information about version
-v, --verbose Be verbose
-d <destination directory>,
--destdir=<destination directory> Directory for retrieved packages
Default is ./source-<package name>
-f, --force Force overwriting an existing
destdir
-l, --list Don't download but just list versions
--binary Download binary packages instead of
source packages
-a <architecture>,
--architecture <architecture> Specify architecture of binary packages,
implies --binary. May be given multiple
times
Default settings modified by devscripts configuration files or command-line
options:
$modified_conf_msg
EOF
exit $rc;
}
sub fetch_json_page {
my ($json_url) = @_;
# download the json page:
verbose "Getting json $json_url\n";
my $content = LWP::Simple::get($json_url);
return unless defined $content;
my $json = JSON::PP->new();
verbose "Received json:\n$content\n";
# these are some nice json options to relax restrictions a bit:
my $json_text = $json->allow_nonref->utf8->relaxed->decode($content);
return $json_text;
}
sub read_conf {
my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
%config_vars = (
'DEBSNAP_VERBOSE' => 'no',
'DEBSNAP_DESTDIR' => '',
'DEBSNAP_BASE_URL' => 'https://snapshot.debian.org',
);
my %config_default = %config_vars;
my $shell_cmd;
# Set defaults
$shell_cmd .= qq[unset `set | grep "^DEBSNAP_" | cut -d= -f1`;\n];
foreach my $var (keys %config_vars) {
$shell_cmd .= qq[$var="$config_vars{$var}";\n];
}
$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
# Read back values
foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
my $shell_out = `/bin/bash -c '$shell_cmd'`;
@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
# Check validity
$config_vars{'DEBSNAP_VERBOSE'} =~ /^(yes|no)$/
or $config_vars{'DEBSNAP_VERBOSE'} = 'no';
foreach my $var (sort keys %config_vars) {
if ($config_vars{$var} ne $config_default{$var}) {
$modified_conf_msg .= " $var=$config_vars{$var}\n";
}
}
$modified_conf_msg ||= " (none)\n";
chomp $modified_conf_msg;
$opt{verbose} = $config_vars{DEBSNAP_VERBOSE} eq 'yes';
$opt{destdir} = $config_vars{DEBSNAP_DESTDIR};
$opt{baseurl} = $config_vars{DEBSNAP_BASE_URL};
}
sub have_file($$) {
my ($path, $hash) = @_;
if (-e $path) {
open(HASH, '-|', 'sha1sum', $path) || fatal "Can't run sha1sum: $!";
while (<HASH>) {
if (m/^([a-fA-F\d]{40}) /) {
close(HASH) || fatal "sha1sum problems: $! $?";
return $1 eq $hash;
}
}
}
return 0;
}
sub fatal($) {
my ($pack, $file, $line);
($pack, $file, $line) = caller();
(my $msg = "$progname: fatal error at line $line:\n@_\n") =~ tr/\0//d;
$msg =~ s/\n\n$/\n/;
$! = 1;
die $msg;
}
sub verbose($) {
(my $msg = "@_\n") =~ tr/\0//d;
$msg =~ s/\n\n$/\n/;
print "$msg" if $opt{verbose};
}
sub keep_version($) {
my $version = shift;
if (defined $pkgversion) {
return version_compare_relation($pkgversion, REL_EQ, $version);
}
if (defined $firstversion) {
if ($firstversion > $version) {
verbose "skip version $version: older than first";
return 0;
}
}
if (defined $lastversion) {
if ($lastversion < $version) {
verbose "skip version $version: newer than last";
return 0;
}
}
return 1;
}
###
# Main program
###
read_conf(@ARGV);
Getopt::Long::Configure('gnu_compat');
Getopt::Long::Configure('no_ignore_case');
GetOptions(
\%opt, 'verbose|v', 'destdir|d=s', 'force|f',
'help|h', 'version', 'first=s', 'last=s',
'list|l', 'binary', 'architecture|a=s@'
) || usage(1);
usage(0) if $opt{help};
version() if $opt{version};
usage(1) unless @ARGV;
$package = shift;
if (@ARGV) {
my $version = shift;
$pkgversion = Dpkg::Version->new($version);
fatal "Invalid version '$version'" unless $pkgversion->is_valid();
}
if (defined $opt{first}) {
$firstversion = Dpkg::Version->new($opt{first});
fatal "Invalid version '$opt{first}'" unless $firstversion->is_valid();
}
if (defined $opt{last}) {
$lastversion = Dpkg::Version->new($opt{last});
fatal "Invalid version '$opt{last}'" unless $lastversion->is_valid();
}
$package eq '' && usage(1);
$opt{binary} ||= @{ $opt{architecture} };
my $baseurl;
if ($opt{binary}) {
$opt{destdir} ||= "binary-$package";
$baseurl = "$opt{baseurl}/mr/binary/$package/";
} else {
$opt{destdir} ||= "source-$package";
$baseurl = "$opt{baseurl}/mr/package/$package/";
}
my $mkdir_done = 0;
my $mkDestDir = sub {
unless ($mkdir_done) {
if (-d $opt{destdir}) {
unless ($opt{force} || cwd() eq abs_path($opt{destdir})) {
fatal
"Destination dir $opt{destdir} already exists.\nPlease (re)move it first, or use --force to overwrite.";
}
}
make_path($opt{destdir});
$mkdir_done = 1;
}
};
my $json_text = fetch_json_page($baseurl);
unless ($json_text && @{ $json_text->{result} }) {
fatal "Unable to retrieve information for $package from $baseurl.";
}
my @versions = @{ $json_text->{result} };
@versions
= $opt{binary}
? grep { keep_version($_->{binary_version}) } @versions
: grep { keep_version($_->{version}) } @versions;
unless (@versions) {
warn "$progname: No matching versions found for $package\n";
$warnings++;
}
if ($opt{list}) {
foreach my $version (@versions) {
if ($opt{binary}) {
print "$version->{binary_version}\n";
} else {
print "$version->{version}\n";
}
}
} elsif ($opt{binary}) {
foreach my $version (@versions) {
my $src_json
= fetch_json_page(
"$opt{baseurl}/mr/package/$version->{source}/$version->{version}/binfiles/$version->{name}/$version->{binary_version}?fileinfo=1"
);
unless ($src_json) {
warn
"$progname: No binary packages found for $package version $version->{binary_version}\n";
$warnings++;
next;
}
my @results = @{ $src_json->{result} };
if (@{ $opt{architecture} }) {
my %archs = map { ($_ => 0) } @{ $opt{architecture} };
@results = grep {
exists $archs{ $_->{architecture} }
&& ++$archs{ $_->{architecture} }
} @results;
my @missing = grep { $archs{$_} == 0 } sort keys %archs;
if (@missing) {
warn
"$progname: No binary packages found for $package version $version->{binary_version} on "
. join(', ', @missing) . "\n";
$warnings++;
}
}
foreach my $result (@results) {
my $hash = $result->{hash};
my $fileinfo = @{ $src_json->{fileinfo}{$hash} }[0];
my $file_url = "$opt{baseurl}/file/$hash";
my $file_name = basename($fileinfo->{name});
if (!have_file("$opt{destdir}/$file_name", $hash)) {
verbose "Getting file $file_name: $file_url";
$mkDestDir->();
LWP::Simple::mirror($file_url, "$opt{destdir}/$file_name");
}
}
}
} else {
foreach my $version (@versions) {
my $src_json
= fetch_json_page("$baseurl$version->{version}/srcfiles?fileinfo=1");
unless ($src_json) {
warn
"$progname: No source files found for $package version $version->{version}\n";
$warnings++;
next;
}
# Get the dsc file and parse it to get the list of files to be
# restored (this should fix most issues with multi-tarball
# source packages):
my $dsc_name;
my $dsc_hash;
foreach my $hash (keys %{ $src_json->{fileinfo} }) {
my $fileinfo = $src_json->{fileinfo}{$hash};
foreach my $info (@$fileinfo) {
if ($info->{name} =~ m/^\Q${package}\E_.*\.dsc/) {
$dsc_name = $info->{name};
$dsc_hash = $hash;
last;
}
}
last if $dsc_name;
}
unless ($dsc_name) {
warn
"$progname: No dsc file detected for $package version $version->{version}\n";
$warnings++;
next;
}
# Retrieve the dsc file:
my $file_url = "$opt{baseurl}/file/$dsc_hash";
if (!have_file("$opt{destdir}/$dsc_name", $dsc_hash)) {
verbose "Getting dsc file $dsc_name: $file_url";
$mkDestDir->();
LWP::Simple::mirror($file_url, "$opt{destdir}/$dsc_name");
}
# Get the list of files from the dsc:
my @files;
open my $fh, '<', "$opt{destdir}/$dsc_name"
or die "unable to open the dsc file $opt{destdir}/$dsc_name";
while (<$fh> !~ /^Files:/) { }
while (<$fh> =~ /^ (\S+) (\d+) (\S+)$/) {
my ($checksum, $size, $file) = ($1, $2, $3);
push @files, $file;
}
close $fh
or die "unable to close the dsc file";
# Iterate over files and find the right contents:
foreach my $file_name (@files) {
my $file_hash;
foreach my $hash (keys %{ $src_json->{fileinfo} }) {
my $fileinfo = $src_json->{fileinfo}{$hash};
foreach my $info (@{$fileinfo}) {
if ($info->{name} eq $file_name) {
$file_hash = $hash;
last;
}
}
last if $file_hash;
}
unless ($file_hash) {
# Warning: this next statement will only move to the
# next files, not the next package
print
"$progname: No hash found for file $file_name needed by $package version $version->{version}\n";
$warnings++;
next;
}
my $file_url = "$opt{baseurl}/file/$file_hash";
$file_name = basename($file_name);
if (!have_file("$opt{destdir}/$file_name", $file_hash)) {
verbose "Getting file $file_name: $file_url";
$mkDestDir->();
LWP::Simple::mirror($file_url, "$opt{destdir}/$file_name");
}
}
if ($dsc_name) {
my $dscverifyexe = 'dscverify';
if ($progname eq "scripts/debsnap.pl" && -x "scripts/dscverify.pl")
{
$dscverifyexe = "scripts/dscverify.pl";
}
system $dscverifyexe, ("$opt{destdir}/$dsc_name");
}
}
}
if ($warnings) {
exit 2;
}
exit 0;

462
scripts/debuild.1 Normal file
View file

@ -0,0 +1,462 @@
.TH DEBUILD 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
debuild \- build a Debian package
.SH SYNOPSIS
\fBdebuild\fR [\fIdebuild options\fR] [\fIdpkg-buildpackage options\fR]
[\fB\-\-lintian-opts\fR \fIlintian options\fR]
.br
\fBdebuild\fR [\fIdebuild options\fR] \-\-
\fBbinary\fR|\fBbinary-arch\fR|\fBbinary-indep\fR|\fBclean\fR ...
.SH DESCRIPTION
\fBdebuild\fR creates all the files necessary for uploading a Debian
package. It first runs \fBdpkg-buildpackage\fR, then runs
\fBlintian\fR on the \fI.changes\fR file created
(assuming that \fBlintian\fR is installed), and
finally signs the appropriate files (using \fBdebsign\fR(1) to do
this instead of \fBdpkg-buildpackage\fR(1) itself; all relevant
key-signing options are passed on).
Signing will be skipped if the distribution is \fIUNRELEASED\fR, unless
\fBdpkg-buildpackage\fR's \fB\-\-force-sign\fR option is used.
Parameters can be passed to \fBdpkg-buildpackage\fR
and \fBlintian\fR, where the parameters to the latter are
indicated with the \fB\-\-lintian-opts\fR option.
The allowable options in this case are
\fB\-\-lintian\fR and \fB\-\-no-lintian\fR to force or skip the
\fBlintian\fR step, respectively. The default is to run
\fBlintian\fR. There are also various options
available for setting and preserving environment variables, as
described below in the Environment Variables section. In this method
of running \fBdebuild\fR, we also save a build log to the
file \fI../<package>_<version>_<arch>.build\fR.
.PP
An alternative way of using \fBdebuild\fR is to use one or more of the
parameters \fBbinary\fR, \fBbinary-arch\fR, \fBbinary-indep\fR and
\fBclean\fR, in which case \fBdebuild\fR will attempt to gain root
privileges and then run \fIdebian/rules\fR with the given parameters.
A \fB\-\-rootcmd=\fIgain-root-command\fR or
\fB\-r\fIgain-root-command\fR option may be used to specify a method
of gaining root privileges. The \fIgain-root-command\fR is likely to
be one of \fIfakeroot\fR, \fIsudo\fR or \fIsuper\fR. See below for
further discussion of this point. Again, the environment preservation
options may be used. In this case, \fBdebuild\fR will also attempt to
run \fBdpkg-checkbuilddeps\fR first; this can be explicitly requested
or switched off using the options \fB\-D\fR and \fB\-d\fR
respectively. Note also that if either of these or a \fB\-r\fR option
is specified in the configuration file option
\fBDEBUILD_DPKG_BUILDPACKAGE_OPTS\fR, then it will be recognised even in
this method of invocation of \fBdebuild\fR.
.PP
\fBdebuild\fR also reads the \fBdevscripts\fR configuration files as
described below. This allows default options to be given.
.SH "Directory name checking"
In common with several other scripts in the \fBdevscripts\fR package,
\fBdebuild\fR will climb the directory tree until it finds a
\fIdebian/changelog\fR file before attempting to build the package.
As a safeguard against stray files causing potential problems, it will
examine the name of the parent directory once it finds the
\fIdebian/changelog\fR file, and check that the directory name
corresponds to the package name. Precisely how it does this is
controlled by two configuration file variables
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR and \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR, and
their corresponding command-line options \fB\-\-check-dirname-level\fR
and \fB\-\-check-dirname-regex\fR.
.PP
\fBDEVSCRIPTS_CHECK_DIRNAME_LEVEL\fR can take the following values:
.TP
.B 0
Never check the directory name.
.TP
.B 1
Only check the directory name if we have had to change directory in
our search for \fIdebian/changelog\fR. This is the default behaviour.
.TP
.B 2
Always check the directory name.
.PP
The directory name is checked by testing whether the current directory
name (as determined by \fBpwd\fR(1)) matches the regex given by the
configuration file option \fBDEVSCRIPTS_CHECK_DIRNAME_REGEX\fR or by the
command line option \fB\-\-check-dirname-regex\fR \fIregex\fR. Here
\fIregex\fR is a Perl regex (see \fBperlre\fR(3perl)), which will be
anchored at the beginning and the end. If \fIregex\fR contains a '/',
then it must match the full directory path. If not, then it must
match the full directory name. If \fIregex\fR contains the string
\'PACKAGE', this will be replaced by the source package name, as
determined from the \fIchangelog\fR. The default value for the regex is:
\'PACKAGE(-.+)?', thus matching directory names such as PACKAGE and
PACKAGE-version.
.SH ENVIRONMENT VARIABLES
As environment variables can affect the building of a package, often
unintentionally, \fBdebuild\fR sanitises the environment by removing
all environment variables except for \fBTERM\fR, \fBHOME\fR, \fBLOGNAME\fR,
\fBGNUPGHOME\fR, \fBPGPPATH\fR, \fBGPG_AGENT_INFO\fR, \fBGPG_TTY\fR,
\fBDBUS_SESSION_BUS_ADDRESS\fR, \fBFAKEROOTKEY\fR, \fBDEBEMAIL\fR,
\fBDEB_\fI*\fR, the (\fBC\fR, \fBCPP\fR, \fBCXX\fR, \fBLD\fR and
\fBF\fR)\fBFLAGS\fR variables and their \fB_APPEND\fR counterparts and the
locale variables \fBLANG\fR and \fBLC_\fI*\fR. \fBTERM\fR is set to `dumb'
if it is unset, and \fBPATH\fR is set to "/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11".
.PP
If a particular environment variable is required to be passed through
untouched to the build process, this may be specified by using a
\fB\-\-preserve-envvar\fR \fIenvvar\fR (which can also be written as
\fB\-e\fR \fIenvvar\fR option). The environment may be left untouched
by using the \fB\-\-preserve-env\fR option. However, even in this
case, the \fBPATH\fR will be set to the sane value described above. The
\fBonly\fR way to prevent \fBPATH\fR from being reset is to specify a
\fB\-\-preserve-envvar PATH\fR option. But you are warned that using
programs from non-standard locations can easily result in the package
being broken, as it will not be able to be built on standard systems.
.PP
Note that one may add directories to the beginning of the sanitised
\fBPATH\fR, using the \fB\-\-prepend\-path\fR option. This is useful when
one wishes to use tools such as \fBccache\fR or \fBdistcc\fR for building.
.PP
It is also possible to avoid having to type something like
\fIFOO\fB=\fIbar \fBdebuild \-e \fIFOO\fR by writing \fBdebuild \-e
\fIFOO\fB=\fIbar\fR or the long form \fBdebuild \-\-set\-envvar
\fIFOO\fB=\fIbar\fR.
.SH "SUPERUSER REQUIREMENTS"
\fBdebuild\fR needs to be run as superuser to function properly.
There are three fundamentally different ways to do this. The first,
and preferable, method is to use some root-gaining command. The best
one to use is probably \fBfakeroot\fR(1), since it does not involve
granting any genuine privileges. \fBsuper\fR(1) and \fBsudo\fR(1) are
also possibilities. If no \fB\-r\fR (or \fB\-\-rootcmd\fR) option is
given (and recall that \fBdpkg-buildpackage\fR also accepts a \fB\-r\fR
option) and neither of the following methods is used, then
\fB\-rfakeroot\fR will silently be assumed.
.PP
The second method is to use some command such as \fBsu\fR(1) to become
root, and then to do everything as root. Note, though, that
\fBlintian\fR will abort if it is run as root or setuid root; this can
be overcome using the \fB\-\-allow-root\fR option of \fBlintian\fR if
you know what you are doing.
.PP
The third possible method is to have \fBdebuild\fR installed as setuid
root. This is not the default method, and will have to be installed
as such by the system administrator. It must also be realised that
anyone who can run \fBdebuild\fR as root or setuid root has \fBfull
access to the whole machine\fR. This method is therefore not
recommended, but will work. \fBdebuild\fR could be installed with
mode 4754, so that only members of the owning group could run it. A
disadvantage of this method would be that other users would then not
be able to use the program. There are many other variants of this
option involving multiple copies of \fBdebuild\fR, or the use of
programs such as \fBsudo\fR or \fBsuper\fR to grant root privileges to
users selectively. If the sysadmin wishes to do this, she should use
the \fBdpkg-statoverride\fR program to change the permissions of
\fI/usr/bin/debuild\fR. This will ensure that these permissions are
preserved across upgrades.
.SH HOOKS
\fBdebuild\fR supports a number of hooks when running
\fBdpkg\-buildpackage\fR. Note that the hooks \fBdpkg-buildpackage\fR
to \fBlintian\fR (inclusive) are passed through to \fBdpkg-buildpackage\fR
using its corresponding \fB\-\-hook-\fR\fIname\fR option. The available
hooks are as follows:
.TP
\fBdpkg-buildpackage-hook
Run before \fBdpkg-buildpackage\fR begins by calling \fBdpkg-checkbuilddeps\fR.
.IP
Hook is run inside the unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBinit\fR hook.
.TP
\fBclean-hook
Run before \fBdpkg-buildpackage\fR runs \fBdebian/rules clean\fR to clean the
source tree. (Run even if the tree is not being cleaned because \fB\-nc\fR
is used.)
.IP
Hook is run inside the unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBpreclean\fR hook.
.TP
\fBdpkg-source-hook
Run after cleaning the tree and before running \fBdpkg-source\fR. (Run even
if \fBdpkg-source\fR is not being called because \fB\-b\fR, \fB\-B\fR, or \fB\-A\fR is used.)
.IP
Hook is run inside the unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBsource\fR hook.
.TP
\fBdpkg-build-hook\fR
Run after \fBdpkg-source\fR and before calling \fBdebian/rules build\fR. (Run
even if this is a source-only build, so \fBdebian/rules build\fR is not
being called.)
.IP
Hook is run inside the unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBbuild\fR hook.
.TP
\fBdpkg-binary-hook
Run between \fBdebian/rules build\fR and \fBdebian/rules binary\fR(\fB\-arch\fR). Run
\fBonly\fR if a binary package is being built.
.IP
Hook is run inside the unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBbinary\fR hook.
.TP
\fBdpkg-genchanges-hook
Run after the binary package is built and before calling
\fBdpkg-genchanges\fR.
.IP
Hook is run inside the unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBchanges\fR hook.
.TP
\fBfinal-clean-hook
Run after \fBdpkg-genchanges\fR and before the final \fBdebian/rules clean\fR.
(Run even if we are not cleaning the tree post-build, which is the
default.)
.IP
Hook is run inside the unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBpostclean\fR hook.
.TP
\fBlintian-hook
Run (once) before calling \fBlintian\fR. (Run even if we are
not calling \fBlintian\fR.)
.IP
Hook is run from parent directory of unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBcheck\fR hook.
.TP
\fBsigning-hook
Run after calling \fBlintian\fR before any signing takes place.
(Run even if we are not signing anything.)
.IP
Hook is run from parent directory of unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBsign\fR hook, but is run by \fBdebuild\fR.
.TP
\fBpost-dpkg-buildpackage-hook
Run after everything has finished.
.IP
Hook is run from parent directory of unpacked source.
.IP
Corresponds to \fBdpkg\fR's \fBdone\fR hook, but is run by \fBdebuild\fR.
.PP
A hook command can be specified either in the configuration file as,
for example, DEBUILD_SIGNING_HOOK='foo' (note the hyphens change into
underscores!) or as a command line option \fB\-\-signing\-hook-foo\fR.
The command will have certain percent substitutions made on it: \fB%%\fR
will be replaced by a single \fB%\fR sign, \fB%p\fR will be replaced by the
package name, \fB%v\fR by the package version number, \fB%s\fR by the source
version number, \fB%u\fR by the upstream version number. Neither \fB%s\fR nor \fB%u\fR
will contain an epoch. \fB%a\fR will be \fB1\fR if the immediately following
action is to be performed and \fB0\fR if not (for example, in the
\fBdpkg-source\fR hook, \fB%a\fR will become \fB1\fR if \fBdpkg-source\fR is to be run and \fB0\fR
if not). Then it will be handed to the shell to deal with, so it can
include redirections and stuff. For example, to only run the
\fBdpkg-source\fR hook if \fBdpkg-source\fR is to be run, the hook could be
something like: "if [ %a \-eq 1 ]; then ...; fi".
.PP
\fBPlease take care with hooks\fR, as misuse of them can lead to
packages which FTBFS (fail to build from source). They can be useful
for taking snapshots of things or the like.
.SH "OPTIONS"
For details, see above.
.TP
.B \-\-no-conf\fR, \fB\-\-noconf
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
.BI \-\-rootcmd= "gain-root-command\fR, " \-r gain-root-command
Command to gain root (or fake root) privileges.
.TP
.B \-\-preserve\-env
Do not clean the environment, except for PATH.
.TP
.BI \-\-preserve\-envvar= "var\fR, " \-e var
Do not clean the \fIvar\fR variable from the environment.
.IP
If \fIvar\fR ends in an asterisk ("*") then all variables with names
that match the portion of \fIvar\fR before the asterisk will be
preserved.
.TP
.BI \-\-set\-envvar= var = "value\fR, " \-e var = value
Set the environment variable \fIvar\fR to \fIvalue\fR and do not
remove it from the environment.
.TP
.BI \-\-prepend\-path= "value "
Once the normalized PATH has been set, prepend \fIvalue\fR
to it.
.TP
.B \-\-lintian
Run \fBlintian\fR after \fBdpkg-buildpackage\fR. This is the default
behaviour, and it overrides any configuration file directive to the
contrary.
.TP
.B \-\-no\-lintian
Do not run \fBlintian\fR after \fBdpkg-buildpackage\fR.
.TP
.B \-\-no\-tgz\-check
Even if we're running \fBdpkg-buildpackage\fR and the version number
has a Debian revision, do not check that the \fI.orig.tar.gz\fR file or \fI.orig\fR
directory exists before starting the build.
.TP
.B \-\-tgz\-check
If we're running \fBdpkg-buildpackage\fR and the version number has a
Debian revision, check that the \fI.orig.tar.gz\fR file or \fI.orig\fR directory
exists before starting the build. This is the default behaviour.
.TP
\fB\-\-username\fR \fIusername\fR
When signing, use \fBdebrsign\fR instead of \fBdebsign\fR.
\fIusername\fR specifies the credentials to be used.
.TP
\fB\-\-\fIfoo\fB\-hook\fR=\fIhook\fR
Set a hook as described above. If \fIhook\fR is blank, this unsets
the hook.
.TP
\fB\-\-clear\-hooks\fR
Clears all hooks. They may be reinstated by later command line
options.
.TP
\fB\-\-check-dirname-level\fR \fIN\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-\-check-dirname-regex\fR \fIregex\fR
See the above section \fBDirectory name checking\fR for an explanation of
this option.
.TP
\fB\-d\fR
Do not run \fBdpkg-checkbuilddeps\fR to check build dependencies.
.TP
\fB\-D\fR
Run \fBdpkg-checkbuilddeps\fR to check build dependencies.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced by a shell in that order to set
configuration variables. Command line options can be used to override
some of these configuration file settings, otherwise the
\fB\-\-no\-conf\fR option can be used to prevent reading these files.
Environment variable settings are ignored when these configuration
files are read. The currently recognised variables are:
.TP
.B DEBUILD_PRESERVE_ENV
If this is set to \fIyes\fR, then it is the same as the
\fB\-\-preserve\-env\fR command line parameter being used.
.TP
.B DEBUILD_PRESERVE_ENVVARS
Which environment variables to preserve. This should be a
comma-separated list of variables. This corresponds to using possibly
multiple \fB\-\-preserve\-envvar\fR or \fB\-e\fR options.
.TP
.BI DEBUILD_SET_ENVVAR_ var = value
This corresponds to \fB\-\-set\-envvar=\fIvar\fB=\fIvalue\fR.
.TP
.B DEBUILD_PREPEND_PATH
This corresponds to \fB\-\-prepend\-path\fR.
.TP
.B DEBUILD_ROOTCMD
Setting this variable to \fIprog\fR is the equivalent of
\fB\-r\fIprog\fR.
.TP
.B DEBUILD_TGZ_CHECK
Setting this variable to \fIno\fR is the same as the
\fB\-\-no\-tgz\-check\fR command line option.
.TP
.B DEBUILD_SIGNING_USERNAME
Setting this variable is the same as using the \fB\-\-username\fR
command line option.
.TP
.B DEBUILD_DPKG_BUILDPACKAGE_OPTS
These are options which should be passed to the invocation of
\fBdpkg-buildpackage\fR. They are given before any command-line
options. Due to issues of shell quoting, if a word containing spaces
is required as a single option, extra quotes will be required. For
example, to ensure that your own GPG key is always used, even for
sponsored uploads, the configuration file might contain the line:
.IP
.nf
DEBUILD_DPKG_BUILDPACKAGE_OPTS="\-k'Julian Gilbey <jdg@debian.org>' \-sa"
.fi
.IP
which gives precisely two options. Without the extra single quotes,
\fBdpkg-buildpackage\fR would reasonably complain that \fIGilbey\fR is
an unrecognised option (it doesn't start with a \fB\-\fR sign).
.IP
Also, if this option contains any \fB\-r\fR, \fB\-d\fR or \fB\-D\fR
options, these will always be taken account of by \fBdebuild\fR. Note
that a \fB\-r\fR option in this variable will override the setting in
.BR DEBUILD_ROOTCMD .
.TP
\fBDEBUILD_\fIFOO\fB_HOOK
The hook variable for the \fIfoo\fR hook. See the section on hooks
above for more details. By default, this is empty.
.TP
.B DEBUILD_LINTIAN
Should we run \fBlintian\fR? If this is set to \fIno\fR, then
\fBlintian\fR will not be run.
.TP
.B DEBUILD_LINTIAN_OPTS
These are options which should be passed to the invocation of
\fBlintian\fR. They are given before any command-line options, and
the usage of this variable is as described for the
\fBDEBUILD_DPKG_BUILDPACKAGE_OPTS\fR variable.
.TP
.BR DEVSCRIPTS_CHECK_DIRNAME_LEVEL ", " DEVSCRIPTS_CHECK_DIRNAME_REGEX
See the above section \fBDirectory name checking\fR for an explanation of
these variables. Note that these are package-wide configuration
variables, and will therefore affect all \fBdevscripts\fR scripts
which check their value, as described in their respective manpages and
in \fBdevscripts.conf\fR(5).
.SH EXAMPLES
To build your own package, simply run \fBdebuild\fR from inside the
source tree. \fBdpkg-buildpackage\fR(1) options may be given on the
command line.
.PP
The typical command line options to build only the binary package(s)
without signing the .changes file (or the non-existent .dsc file):
.IP
.nf
debuild \-i \-us \-uc \-b
.fi
.PP
Change the \fB\-b\fR to \fB\-S\fR to build only a source package.
.PP
An example using \fBlintian\fR to check the
resulting packages and passing options to it:
.IP
.nf
debuild \-\-lintian-opts \-i
.fi
.PP
Note the order of options here: the \fBdebuild\fR options come first,
then the \fBdpkg-buildpackage\fR ones, then finally the checker
options. (And \fBlintian\fR is called by default.) If you find
yourself using the same \fBdpkg-buildpackage\fR options repeatedly,
consider using the \fBDEBUILD_DPKG_BUILDPACKAGE_OPTS\fR configuration file
option as described above.
.PP
To build a package for a sponsored upload, given
\fIfoobar_1.0-1.dsc\fR and the respective source files, run something
like the following commands:
.IP
.nf
dpkg-source \-x foobar_1.0-1.dsc
cd foobar-1.0
debuild \-k0x12345678
.fi
.PP
where 0x12345678 is replaced by your GPG key ID or other key
identifier such as your email address. Again, you could also use the
\fBDEBUILD_DPKG_BUILDPACKAGE_OPTS\fR configuration file option as described
above to avoid having to type the \fB\-k\fR option each time you do a
sponsored upload.
.SH "SEE ALSO"
.BR chmod (1),
.BR debsign (1),
.BR dpkg-buildpackage (1),
.BR dpkg-checkbuilddeps (1),
.BR fakeroot (1),
.BR lintian (1),
.BR su (1),
.BR sudo (1),
.BR super (1),
.BR devscripts.conf (5),
.BR dpkg-statoverride (8)
.SH AUTHOR
The original \fBdebuild\fR program was written by Christoph Lameter
<clameter@debian.org>. The current version has been written by Julian
Gilbey <jdg@debian.org>.

View file

@ -0,0 +1,113 @@
# /usr/share/bash-completion/completions/debuild
# Bash command completion for debuild(1).
# Documentation: bash(1), section “Programmable Completion”.
# Copyright © 2015, Nicholas Bamber <nicholas@periapt.co.uk>
_debuild()
{
local cur prev words cword i _options special _prefix
_init_completion || return
for (( i=${#words[@]}-1; i > 0; i-- )); do
if [[ ${words[i]} == @(binary|binary-arch|binary-indep|clean|--lintian-opts) ]]; then
special=${words[i]}
break
fi
done
if [[ -n $special ]]; then
case $special in
--lintian-opts)
case $prev in
--include-dir)
COMPREPLY=( $( compgen -o filenames -d -- "$cur" ) )
return 0
;;
--tags-from-file|--cfg|--suppress-tags-from-file)
COMPREPLY=( $( compgen -o filenames -f -- "$cur" ) )
return 0
;;
--color)
COMPREPLY=( $( compgen -W 'never always auto html' -- "$cur" ) )
return 0
;;
--display-source)
COMPREPLY=( $( compgen -W 'policy devref' -- "$cur" ) )
return 0
;;
esac
COMPREPLY=( $( compgen -W '-C --ftp-master-rejects --tags --tags-from-file --color --default-display-level --display-source --display-experimental --no-display-experimental --fail-on-warnings --info --display-info --no-override --pedantic --show-overrides --suppress-tags --suppress-tags-from-file --cfg --no-cfg --ignore-lintian-env --include-dir' -- "$cur" ) )
return 0
;;
*)
COMPREPLY=( $( compgen -W 'binary binary-arch binary-indep clean' -- "$cur" ) )
return 0
;;
esac
fi
case $prev in
--rootcmd)
_options=
for i in fakeroot super sudo
do
which $i > /dev/null && _options+=" ${i}"
done
COMPREPLY=( $( compgen -W "${_options}" -- "$cur" ) )
return 0
;;
--preserve-envvar)
COMPREPLY=( $( compgen -o nospace -e -- "$cur" ) )
return 0
;;
--set-envvar)
COMPREPLY=( $( compgen -o nospace -e -S'=' -- "$cur" ) )
return 0
;;
--prepend-path|--admindir)
COMPREPLY=( $( compgen -o filenames -d -- "$cur" ))
return 0
;;
--check-dirname-level)
COMPREPLY=( $( compgen -W '0 1 2' -- "$cur" ) )
return 0
;;
-j)
COMPREPLY=( $( compgen -W 'auto 1 2 3 4 5 6' -- "$cur" ) )
return 0
;;
esac
if [[ "$cur" == -* ]]; then
_options='--preserve-envvar --set-envvar --rootcmd --preserve-env
--prepend-path --lintian --no-lintian --no-tgz-check --tgz-check
--username --clear-hooks --check-dirname-level --check-dirname-regex -d
-D --dpkg-buildpackage-hook --clean-hook --dpkg-source-hook
--dpkg-build-hook --dpkg-binary-hook --dpkg-genchanges-hook
--final-clean-hook --lintian-hook signing-hook
post-dpkg-buildpackage-hook --lintian-opts -g -G -b -B -A -S -F -si -sa
-sd -v -C -m -e -a --host-type --target-arch --target-type -P -j -D -d
-nc -tc --admindir --changes-options --source-options -z -Z -i -I -sn
-ss -sA -sk -su -sr -sK -sU -sR --force-sign -us -uc -k -p
--check-option --check-command -R -r'
if [[ "$prev" == debuild ]]; then
_options+=' --no-conf'
fi
COMPREPLY=( $( compgen -W "${_options}" -- "$cur" ) )
else
COMPREPLY=( $( compgen -W 'binary binary-arch binary-indep clean' -- "$cur" ) )
fi
return 0
} &&
complete -F _debuild debuild
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

1230
scripts/debuild.pl Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,49 @@
.TH DEP-14-CONVERT-GIT-BRANCH-NAMES 1 "Debian Utilities" "DEBIAN"
.SH NAME
dep-14-convert-git-branch-names \- Convert git repository branch names to follow DEP-14.
.SH DESCRIPTION
This helper tool assists in renaming the branch names by printing the necessary
git commands for local repository and salsa commands remote repository to rename
the branches and to update the default git branch. It also prints commands to
create a gbp.conf with matching branch names.
.PP
As this script does not actually modify anything, so feel free to run this
script in any Debian packaging repository to see what it outputs.
.
Renaming is needed as git defaults to 'main' as the branch name. Previously git
used 'master', and git-buildpackage still used 'master' as the branch name. This
is not ideal for Debian packaging, as using the same default development branch
names as upstream projects typically do may lead into branch name conflicts.
.PP
The DEP-14 (https://dep-team.pages.debian.net/deps/dep14/, status: candidate)
states:
.PP
In Debian this means that uploads to unstable and experimental should be
prepared either in the debian/latest branch or respectively in the
debian/unstable and debian/experimental branches.
.PP
and:
.PP
The helper tools that do create those repositories should use a command like
git symbolic-ref HEAD refs/heads/debian/latest to update HEAD to point to the
desired branch.
.SH SYNOPSIS
.B dep-14-convert-git-branch-names
[\fI\,options\/\fR]
.IP
.TP
Options:
.TP
\fB\-\-packaging\-branch\fR <name>
Branch for main packaging (e.g. 'debian/latest')
.TP
\fB\-\-debug\fR
Display debug information while running
.TP
\fB\-h\fR, \fB\-\-help\fR
Display this help message
.TP
\fB\-\-version\fR
Display version information
.SH "SEE ALSO"
DEP-14: https://dep-team.pages.debian.net/deps/dep14/

View file

@ -0,0 +1,516 @@
#!/bin/bash
# This program is used to check that a git repository follows the DEP-14 branch
# naming scheme. If not, it suggests how to convert it.
# Debian dep14-convert. Copyright (C) 2024-2025 Otto Kekäläinen.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# Abort if anything goes wrong
set -euo pipefail
readonly PROGNAME=$(basename "$0")
readonly REQUIRED_FILES=("debian/source/format" "debian/control")
# Global variables
declare -a COMMANDS=()
declare -x SALSA_PROJECT=""
declare debian_branch=""
declare upstream_branch=""
declare APPLY=""
declare DEBUG=""
declare dep14_debian_branch="debian/latest"
stderr() {
echo "$@" >&2
}
error() {
stderr "ERROR: $*"
}
debug() {
if [[ -n "$DEBUG" ]]
then
if [ -z "$*" ]
then
stderr
else
stderr "DEBUG: $*"
fi
fi
}
die() {
error "$*"
exit 1
}
usage() {
printf "%s\n" \
"Usage: $PROGNAME [options]
This helper tool assists in renaming the branch names by printing the necessary
git commands for local repository and salsa commands remote repository to rename
the branches and to update the default git branch. It also prints commands to
create a gbp.conf with matching branch names.
As this script does not actually modify anything, so feel free to run this
script in any Debian packaging repository to see what it outputs.
For DEP-14 purpose and details, please see
https://dep-team.pages.debian.net/deps/dep14/
Options:
--packaging-branch <name> Branch for main packaging (e.g. '${dep14_debian_branch}')
--debug Display debug information while running
-h, --help Display this help message
--version Display version information"
}
version() {
printf "%s\n" \
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
This code is copyright 2024-2025 by Otto Kekäläinen, all rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 3 or later."
}
check_requirements() {
# Check if we're in a git repository
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1
then
die "Not in a git repository. Please run this script from within a git repository"
fi
# Check for required files
for file in "${REQUIRED_FILES[@]}"
do
if [[ ! -f "$file" ]]
then
die "Required file $file not found"
fi
done
}
# Given the output from 'git ls-remote --get-url', parse the Salsa project slug
# Examples:
# https://salsa.debian.org/games-team/vcmi.git => games-team/vcmi
# git@salsa.debian.org:games-team/vcmi.git => games-team/vcmi
find_salsa_remote() {
# Populate SALSA_PROJECT only if it contained a Salsa address, otherwise
# keep it empty to prevent garbage from being passed on
case "$1" in
"git@salsa.debian.org:"*)
SALSA_PROJECT="${1##git@salsa.debian.org:}"
;;
"https://salsa.debian.org/"*)
SALSA_PROJECT="${1##https://salsa.debian.org/}"
;;
esac
SALSA_PROJECT="${SALSA_PROJECT%%.git}"
echo $SALSA_PROJECT
}
# Find the most likely branch used for unstable uploads
find_debian_branch() {
local debian_branch=""
# if debian/gbp.conf exists, use value of debian-branch
if [[ -f debian/gbp.conf ]] && grep -q "^debian-branch" debian/gbp.conf
then
debian_branch=$(grep -oP "^debian-branch[[:space:]]*=[[:space:]]*\K.*" debian/gbp.conf)
debug "debian/gbp.conf exists and has debian-branch '$debian_branch'"
if git rev-parse --verify "$debian_branch" > /dev/null 2>&1
then
echo "$debian_branch"
return
fi
fi
# check debian/changelog on common branches
# and if the changelog targeted 'unstable'
local branches="debian debian/sid debian/unstable debian/master master main"
for branch in $branches
do
debug "Check debian/changelog on branch '$branch'"
# Check if the branch has debian/changelog
local git_contents=$(git ls-tree -r $branch 2>&1 | grep "debian/changelog")
if [[ -n "$git_contents" ]]
then
local changelog_content=$(git show "$branch:debian/changelog")
local distribution=$(echo "$changelog_content" | \
grep "^[a-z]" | \
cut -d ' ' -f 3 | \
grep -v UNRELEASED | \
grep -v experimental | \
grep -m 1 -o "[a-z-]*"
)
debug "Found distribution '$distribution'"
if [[ "$distribution" == "unstable" ]]
then
debian_branch="$branch"
echo "$debian_branch"
return
fi
fi
done
}
# Find the most likely branch used for upstream releases
# - if debian/gbp.conf exists, use value of upstream-branch
# - check if common branches (upstream, master, main) recently merged on the
# assumed debian branch
find_upstream_branch() {
local debian_branch="$1"
local branches=$(git branch --list --format="%(refname:short)")
local upstream_branch=""
# if debian/gbp.conf exists on debian branch, use value of upstream-branch
if [[ -n "$debian_branch" ]] && git show "$debian_branch:debian/gbp.conf" 2>/dev/null | grep "^upstream-branch" > /dev/null
then
upstream_branch=$(git show "$debian_branch:debian/gbp.conf" | grep -oP "^upstream-branch[[:space:]]*=[[:space:]]*\K.*")
if git rev-parse --verify "$upstream_branch" >/dev/null 2>&1
then
echo "$upstream_branch"
return
fi
fi
# Check which branch that modified files outside of debian/ was most
# recently merged on the debian branch, but cap checks to 50 most recent
# merges
merge_commits=$(git log --merges --format="%H" -50 $debian_branch)
# Iterate through the merge commits
for commit in $merge_commits
do
# Get the two parent commits
parent1=$(git rev-parse $commit^1)
parent2=$(git rev-parse $commit^2)
if [[ -n "$DEBUG" ]]
then
debug
debug "commit $commit"
debug git log -1 --oneline $parent1
git log -1 --oneline $parent1 >&2
debug git log -1 --oneline $parent2
git log -1 --oneline $parent2 >&2
fi
# Check if any files outside debian/ were changed as a result of the merge
changed_files=$(git diff --name-only --diff-filter=ACMRTUXB $parent1...$commit | grep -v "^debian/")
# If there are changed files outside debian/, break the loop
if [[ -n "$changed_files" ]]
then
debug "First merge affecting files outside debian/: $commit"
# Get the branch names that decent from the merge commit
merge_branches=$(git branch --list --format="%(refname:short)" --contains $parent2)
#debug "merge_branches: $merge_branches"
for branch in $merge_branches
do
# If only one branch was found, it must be it
if [[ "$branch" == "$merge_branches" ]]
then
upstream_branch="$branch"
break
fi
# If branch has no debian/changelog, assume it was the upstream branch
local git_contents=$(git ls-tree -r $branch 2>&1 | grep "debian/changelog")
if [[ -z "$git_contents" ]]
then
debug "Found branch '$branch' with no 'debian/changelog'"
upstream_branch="$branch"
break
fi
done
echo "$upstream_branch"
return
fi
done
}
# Parse command line arguments
while :
do
case "${1:-}" in
--apply)
# @TODO: Not implemented yet
APPLY=1
shift
;;
--debug)
DEBUG=1
shift
;;
-h | --help)
usage
exit 0
;;
--version)
version
exit 0
;;
--packaging-branch)
shift
dep14_debian_branch="$1"
shift
;;
--)
shift
break
;;
-*)
die "Unknown option: $1"
;;
*)
break
;;
esac
done
# Main script execution starts here
check_requirements
# Check if we have a valid packaging branch name
git check-ref-format --branch "$dep14_debian_branch" >/dev/null
# Check if package is native
if grep -qF native debian/source/format 2>/dev/null
then
stderr "DEP-14 is not applicable to native Debian packages."
grep -HF native debian/source/format
exit 0
fi
# Check for problematic upstream remote
if git remote get-url upstream > /dev/null 2>&1
then
stderr "WARNING: There is a remote called 'upstream', which may interfere with branch names 'upstream/*'."
stderr "Please rename the remote by running: git remote rename upstream upstreamvcs"
stderr
fi
# Check branch count
local_branches=$(git branch --list --format="%(refname:short)")
branch_count=$(echo "$local_branches" | wc -l)
if [[ "$branch_count" -gt 1 ]]
then
stderr "The git repository has the following local branches:" $local_branches
stderr
else
error "To identify the correct debian and upstream branches, there needs to be at least two local branches."
stderr "Currently there are only: " $local_branches
exit 1
fi
# Print DEP-14 requirements
cat >/dev/stderr << 'EOF'
In DEP-14, these branches should exist in the Debian packaging repository:
* debian/latest Used to create the *.debian.tar.xz that contains the Debian
packaging code from the debian/ directory, and which is
uploaded to Debian unstable (or occasionally to experimental).
DEP-14 also allows using branch names debian/unstable
or debian/experimental.
* upstream/latest Used to create the *.orig.tar.gz that contains the unmodified
source code of the specific upstream release.
Optionally, DEP-14 suggests the following branch:
* pristine-tar Contains xdelta data for making the release tarball
bit-for-bit identical with the original one, so that the
upstream *.orig.tar.gz.asc signature can be validated.
Other branches may also exist, but are not required.
EOF
# Check debian/latest branch
stderr -n "-> Branch ${dep14_debian_branch}: "
if git show-ref --verify --quiet refs/heads/${dep14_debian_branch}
then
stderr "exists"
debian_branch="${dep14_debian_branch}"
else
stderr -n "missing"
debian_branch=$(find_debian_branch)
if [[ -n "$debian_branch" ]]
then
stderr ", presumably '$debian_branch' should be renamed"
COMMANDS+=("git branch -m $debian_branch ${dep14_debian_branch}")
# Get Salsa project name primarily from git remote
SALSA_PROJECT="$(find_salsa_remote "$(git ls-remote --get-url)")"
# If nothing matched, maybe there's another remote
if [[ -z "$SALSA_PROJECT" ]]
then
debug "Current git remote not on Salsa, check other remotes"
SALSA_PROJECT=$(
git remote show -n | while read -r remote
do
find_salsa_remote "$(git ls-remote --get-url $remote)" && break || true
done
)
fi
# If nothing matched, fall back to Vcs-Git field
if [[ -z "$SALSA_PROJECT" ]]
then
debug "No git remote on Salsa, using Vcs-Git for SALSA_PROJECT instead"
SALSA_PROJECT=$(find_salsa_remote "$(git show "$debian_branch:debian/control" | grep -oP 'Vcs-Git: \K(.+salsa\.debian\.org.+)')" || true)
fi
if [[ -n "$SALSA_PROJECT" ]]
then
# Unprotecting the branch is a bit ugly, but this is how 'salsa' in
# devscripts works
COMMANDS+=("salsa protect_branch $SALSA_PROJECT $debian_branch no # (intentionally fails with error 404 if branch wasn't protected)")
COMMANDS+=("salsa rename_branch $SALSA_PROJECT --source-branch=$debian_branch --dest-branch=${dep14_debian_branch}")
COMMANDS+=("salsa update_repo $SALSA_PROJECT --rename-head --source-branch=$debian_branch --dest-branch=${dep14_debian_branch}")
fi
else
stderr
die "Could not find the current debian branch"
fi
fi
# Check upstream/latest branch
stderr -n "-> Branch upstream/latest: "
if git show-ref --verify --quiet refs/heads/upstream/latest
then
stderr "exists"
else
stderr -n "missing"
upstream_branch=$(find_upstream_branch "$debian_branch")
if [[ -n "$upstream_branch" ]]
then
stderr ", presumably '$upstream_branch' should be renamed"
COMMANDS+=("git branch -m $upstream_branch upstream/latest")
if [[ -n "$SALSA_PROJECT" ]]
then
# Rename to temporary name before using final name to avoid API error:
# (HTTP 400): Bad Request {"message":"Failed to create branch 'upstream/latest'
COMMANDS+=("salsa rename_branch $SALSA_PROJECT --source-branch=$upstream_branch --dest-branch=temporary")
COMMANDS+=("salsa rename_branch $SALSA_PROJECT --source-branch=temporary --dest-branch=upstream/latest")
fi
else
stderr
die "Could not find the current upstream branch"
fi
fi
# Check gbp.conf configuration
stderr -n "-> Configuration file debian/gbp.conf: "
gbp_conf_defaultsection=false
if git ls-tree -r "$debian_branch" 2>&1 | grep "debian/gbp.conf" > /dev/null
then
stderr -n "exists "
if git show "$debian_branch:debian/gbp.conf" | grep -qP "^debian-branch[[:space:]]*=[[:space:]]*${dep14_debian_branch}" &&
git show "$debian_branch:debian/gbp.conf" | grep -qP "^upstream-branch[[:space:]]*=[[:space:]]*upstream/latest"
then
stderr "and 'debian-branch' and 'upstream-branch' are correctly configured"
else
stderr "but 'debian-branch' or 'upstream-branch' does not have correct values"
COMMANDS+=("git checkout ${dep14_debian_branch}")
if git show "$debian_branch:debian/gbp.conf" | grep -qP "^debian-branch[[:space:]]*="
then
COMMANDS+=("sed -i 's/^debian-branch[[:space:]]*=.*/debian-branch = debian\/latest/' debian/gbp.conf")
else
test "${gbp_conf_defaultsection}" == "true" || COMMANDS+=('echo "[DEFAULT]" >> debian/gbp.conf') && gbp_conf_defaultsection=true
COMMANDS+=("echo \"debian-branch = ${dep14_debian_branch}\" >> debian/gbp.conf")
fi
if git show "$debian_branch:debian/gbp.conf" | grep -qP "^upstream-branch[[:space:]]*="
then
COMMANDS+=("sed -i 's/^upstream-branch[[:space:]]*=.*/upstream-branch = upstream\/latest/' debian/gbp.conf")
else
test "${gbp_conf_defaultsection}" == "true" || COMMANDS+=('echo "[DEFAULT]" >> debian/gbp.conf') && gbp_conf_defaultsection=true
COMMANDS+=('echo "upstream-branch = upstream/latest" >> debian/gbp.conf')
fi
fi
else
stderr "missing"
COMMANDS+=("git checkout ${dep14_debian_branch}")
COMMANDS+=('echo "[DEFAULT]" > debian/gbp.conf')
COMMANDS+=("echo \"debian-branch = ${dep14_debian_branch}\" >> debian/gbp.conf")
COMMANDS+=('echo "upstream-branch = upstream/latest" >> debian/gbp.conf')
fi
# If any commands modified gbp.conf, ensure last command commits everything in git
if echo "${COMMANDS[@]}" | grep --quiet --fixed-strings gbp.conf
then
COMMANDS+=('git commit -a -m "Update git repository layout to follow DEP-14"')
fi
# If any commands ran 'salsa', ensure remote deletes propagate to local git
if echo "${COMMANDS[@]}" | grep --quiet --fixed-strings 'salsa '
then
COMMANDS+=('git pull --prune')
fi
# Blank newline to make output more readable
stderr
# Handle results
if [[ ${#COMMANDS[@]} -eq 0 ]]
then
stderr "Repository is DEP-14 compliant."
else
if [[ -z "$APPLY" ]]
then
stderr "Run the following commands to make the repository follow DEP-14:"
printf " %s\n" "${COMMANDS[@]}"
else
die "Using --apply has not yet been implemented"
# @TODO: Run commands automatically once we have enough confidence they
# always work
fi
fi
if [[ -n "$SALSA_PROJECT" ]]
then
stderr
stderr "For accurate results, ensure your local git checkout is in sync with Salsa project $SALSA_PROJECT."
fi
# Note the developers: When testing changes to this script, a good way to test
# the integration with Salsa is to fork the project
# https://salsa.debian.org/sudo-team/sudo, and in your
# `path-to-fork/-/settings/repository` add `master` as a protected branch. This
# way the salsa API calls will mimic the scenario a typical rename would run
# into. You can delete the fork and create fresh forks for every test as many
# times as needed.

29
scripts/dep3changelog.1 Normal file
View file

@ -0,0 +1,29 @@
.TH DEP3CHANGELOG 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
dep3changelog \- generate a changelog entry from a DEP3-style patch header
.SH SYNOPSIS
\fBdep3changelog\fR \fIpatch\fR [\fIpatch\fR ...] [\fIoptions\fR] [\-\- [\fIdch_options\fR]]
.SH DESCRIPTION
\fBdep3changelog\fR extracts the DEP3 patch headers from the given \fIpatch\fR
files and builds a changelog entry for each patch. If the patch author
differs from the one detected from the \fBDEBEMAIL\fR, \fBNAME\fR,
\fBDEBEMAIL\fR, or \fBEMAIL\fR environment variables, \*(lqThanks to
\fIauthor\fR <\fIemail\fR>\*(rq is added to the changelog entry for that patch.
Any \fBbug-debian\fR or \fBbug-ubuntu\fR fields are added as \*(lqCloses\*(rq to
the changelog entry. The generated changelog entries are passed to
\fBdebchange\fR as an argument along with the given \fIdch_options\fR.
.SH OPTIONS
.TP
.BR \-\-help ", " \-h
Display a help message and exit successfully.
.TP
.B \-\-version
Display version and copyright information and exit successfully.
.SH ENVIRONMENT
.TP
.BR DEBEMAIL ", " EMAIL ", " DEBFULLNAME ", " NAME
See the above description of the use of these environment variables.
.SH AUTHOR
Steve Langasek <vorlon@debian.org>
.SH "SEE ALSO"
.BR debchange (1)

187
scripts/dep3changelog.pl Executable file
View file

@ -0,0 +1,187 @@
#!/usr/bin/perl
# dep3changelog: extract a DEP3 patch header from the named file and
# automatically update debian/changelog with a suitable entry
#
# Copyright 2010 Steve Langasek <vorlon@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
# USA
use 5.008; # We're using PerlIO layers
use strict;
use warnings;
use open ':utf8'; # patch headers are required to be UTF-8
# for checking whether user names are valid and making format() behave
use Encode qw/decode_utf8 encode_utf8/;
use Getopt::Long;
use File::Basename;
# And global variables
my $progname = basename($0);
my %env;
sub usage () {
print <<"EOF";
Usage: $progname patch [patch...] [options] [-- [dch options]]
Options:
--help, -h
Display this help message and exit
--version
Display version information
Additional options specified after -- are passed to dch.
EOF
}
sub version () {
print <<"EOF";
This is $progname, from the Debian devscripts package, version ###VERSION###
This code is copyright 2010 by Steve Langasek, all rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
EOF
}
my ($opt_help, $opt_version);
GetOptions(
"help|h" => \$opt_help,
"version" => \$opt_version,
)
or die
"Usage: $progname patch [... patch] [-- [dch options]]\nRun $progname --help for more details\n";
if ($opt_help) { usage; exit 0; }
if ($opt_version) { version; exit 0; }
my @patches;
while (@ARGV && $ARGV[0] !~ /^-/) {
push(@patches, shift(@ARGV));
}
# Check, sanitise and decode these environment variables
check_env_utf8('DEBFULLNAME');
check_env_utf8('NAME');
check_env_utf8('DEBEMAIL');
check_env_utf8('EMAIL');
if (exists $env{'DEBEMAIL'} and $env{'DEBEMAIL'} =~ /^(.*)\s+<(.*)>$/) {
$env{'DEBFULLNAME'} = $1 unless exists $env{'DEBFULLNAME'};
$env{'DEBEMAIL'} = $2;
}
if (!exists $env{'DEBEMAIL'} or !exists $env{'DEBFULLNAME'}) {
if (exists $env{'EMAIL'} and $env{'EMAIL'} =~ /^(.*)\s+<(.*)>$/) {
$env{'DEBFULLNAME'} = $1 unless exists $env{'DEBFULLNAME'};
$env{'EMAIL'} = $2;
}
}
my $fullname = '';
my $email = '';
if (exists $env{'DEBFULLNAME'}) {
$fullname = $env{'DEBFULLNAME'};
} elsif (exists $env{'NAME'}) {
$fullname = $env{'NAME'};
} else {
my @pw = getpwuid $<;
if ($pw[6]) {
if (my $pw = decode_utf8($pw[6])) {
$pw =~ s/,.*//;
$fullname = $pw;
} else {
warn
"$progname warning: passwd full name field for uid $<\nis not UTF-8 encoded; ignoring\n";
}
}
}
if (exists $env{'DEBEMAIL'}) {
$email = $env{'DEBEMAIL'};
} elsif (exists $env{'EMAIL'}) {
$email = $env{'EMAIL'};
}
for my $patch (@patches) {
my $shebang = 0;
my $dpatch = 0;
# TODO: more than one debian or launchpad bug in a patch?
my ($description, $author, $debbug, $lpbug, $origin);
next unless (open PATCH, $patch);
while (<PATCH>) {
# first line only
if (!$shebang) {
$shebang = 1;
if (/^#!/) {
$dpatch = $shebang = 1;
next;
}
}
last if (/^---/);
chomp;
# only if there was a shebang do we strip comment chars
s/^# // if ($dpatch);
# fixme: this should only apply to the description field.
next if (/^ /);
if (/^(Description|Subject):\s+(.*)\s*/) {
$description = $2;
} elsif (/^(Author|From):\s+(.*)\s*/) {
$author = $2;
} elsif (/^Origin:\s+(.*)\s*/) {
$origin = $1;
} elsif (/^bug-debian:\s+https?:\/\/bugs\.debian\.org\/([0-9]+)\s*/i) {
$debbug = $1;
} elsif (/^bug-ubuntu:\s+https:\/\/.*launchpad\.net\/.*\/([0-9]+)\s*/i)
{
$lpbug = $1;
}
}
close PATCH;
if (!$description || (!$origin && !$author)) {
warn "$patch: Invalid DEP3 header\n";
next;
}
my $changelog = "$patch: $description";
$changelog .= '.' unless ($changelog =~ /\.$/);
if ($author && $author ne $fullname && $author ne "$fullname <$email>") {
$changelog .= " Thanks to $author.";
}
if ($debbug || $lpbug) {
$changelog .= ' Closes';
$changelog .= ": #$debbug" if ($debbug);
$changelog .= "," if ($debbug && $lpbug);
$changelog .= " LP: #$lpbug" if ($lpbug);
$changelog .= '.';
}
system('dch', $changelog, @ARGV);
}
# Is the environment variable valid or not?
sub check_env_utf8 {
my $envvar = $_[0];
if (exists $ENV{$envvar} and $ENV{$envvar} ne '') {
if (!decode_utf8($ENV{$envvar})) {
warn
"$progname warning: environment variable $envvar not UTF-8 encoded; ignoring\n";
} else {
$env{$envvar} = decode_utf8($ENV{$envvar});
}
}
}

View file

@ -0,0 +1,311 @@
# control.py - Represents a debian/control file
#
# Copyright (C) 2010, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""This module implements facilities to deal with Debian control."""
import contextlib
import os
import sys
from devscripts.logger import Logger
try:
import debian.deb822
except ImportError:
Logger.error("Please install 'python3-debian' in order to use this utility.")
sys.exit(1)
try:
from debian._deb822_repro import Deb822ParagraphElement, parse_deb822_file
from debian._deb822_repro.tokens import Deb822Token
HAS_RTS_PARSER = True
except ImportError:
HAS_RTS_PARSER = False
try:
from debian._deb822_repro.formatter import one_value_per_line_formatter
HAS_FULL_RTS_FORMATTING = True
except ImportError:
def one_value_per_line_formatter(
indentation, trailing_separator=True, immediate_empty_line=False
):
raise AssertionError(
"Bug: The dummy one_value_per_line_formatter method should not be called!"
)
HAS_FULL_RTS_FORMATTING = False
def _emit_one_line_value(value_tokens, sep_token, trailing_separator):
first_token = True
yield " "
for token in value_tokens:
if not first_token:
yield sep_token
if not sep_token.is_whitespace:
yield " "
first_token = False
yield token
if trailing_separator and not sep_token.is_whitespace:
yield sep_token
yield "\n"
def wrap_and_sort_formatter(
indentation,
trailing_separator=True,
immediate_empty_line=False,
max_line_length_one_liner=0,
):
"""Provide a formatter that can handle indentation and trailing separators
This is a custom wrap-and-sort formatter capable of supporting wrap-and-sort's
needs. Where possible it delegates to python-debian's own formatter.
:param indentation: Either the literal string "FIELD_NAME_LENGTH" or a positive
integer, which determines the indentation fields. If it is an integer,
then a fixed indentation is used (notably the value 1 ensures the shortest
possible indentation). Otherwise, if it is "FIELD_NAME_LENGTH", then the
indentation is set such that it aligns the values based on the field name.
This parameter only affects values placed on the second line or later lines.
:param trailing_separator: If True, then the last value will have a trailing
separator token (e.g., ",") after it.
:param immediate_empty_line: Whether the value should always start with an
empty line. If True, then the result becomes something like "Field:\n value".
This parameter only applies to the values that will be formatted over more than
one line.
:param max_line_length_one_liner: If greater than zero, then this is the max length
of the value if it is crammed into a "one-liner" value. If the value(s) fit into
one line, this parameter will overrule immediate_empty_line.
"""
if not HAS_FULL_RTS_FORMATTING:
raise NotImplementedError(
"wrap_and_sort_formatter requires python-debian 0.1.44"
)
if indentation != "FIELD_NAME_LENGTH" and indentation < 1:
raise ValueError('indentation must be at least 1 (or "FIELD_NAME_LENGTH")')
# The python-debian library provides support for all cases except cramming
# everything into a single line. So we "only" have to implement the single-line
# case(s) ourselves (which sadly takes plenty of code on its own)
_chain_formatter = one_value_per_line_formatter(
indentation,
trailing_separator=trailing_separator,
immediate_empty_line=immediate_empty_line,
)
if max_line_length_one_liner < 1:
return _chain_formatter
def _formatter(name, sep_token, formatter_tokens):
# We should have unconditionally delegated to the python-debian formatter
# if max_line_length_one_liner was set to "wrap_always"
assert max_line_length_one_liner > 0
all_tokens = list(formatter_tokens)
values_and_comments = [x for x in all_tokens if x.is_comment or x.is_value]
# There are special-cases where you could do a one-liner with comments, but
# they are probably a lot more effort than it is worth investing.
# - If you are here because you disagree, patches welcome. :)
if all(x.is_value for x in values_and_comments):
# We use " " (1 char) or ", " (2 chars) as separated depending on the field.
# (at the time of writing, wrap-and-sort only uses this formatted for
# dependency fields meaning this will be "2" - but now it is future proof).
chars_between_values = 1 + (0 if sep_token.is_whitespace else 1)
# Compute the total line length of the field as the sum of all values
total_len = sum(len(x.text) for x in values_and_comments)
# ... plus the separators
total_len += (len(values_and_comments) - 1) * chars_between_values
# plus the field name + the ": " after the field name
total_len += len(name) + 2
if total_len <= max_line_length_one_liner:
yield from _emit_one_line_value(
values_and_comments, sep_token, trailing_separator
)
return
# If it does not fit in one line, we fall through
# Chain into the python-debian provided formatter, which will handle this
# formatting for us.
yield from _chain_formatter(name, sep_token, all_tokens)
return _formatter
def _insert_after(paragraph, item_before, new_item, new_value):
"""Insert new_item into directly after item_before
New items added to a dictionary are appended."""
try:
paragraph.order_after
except AttributeError:
pass
else:
# Use order_after from python-debian (>= 0.1.42~), which is O(1) performance
paragraph[new_item] = new_value
try:
paragraph.order_after(new_item, item_before)
except KeyError:
# Happens if `item_before` is not present. We ignore this error because we
# are fine with `new_item` ending the "end" of the paragraph in that case.
pass
return
# Old method - O(n) performance
item_found = False
for item in paragraph:
if item_found:
value = paragraph.pop(item)
paragraph[item] = value
if item == item_before:
item_found = True
paragraph[new_item] = new_value
if not item_found:
paragraph[new_item] = new_value
@contextlib.contextmanager
def _open(filename, fd=None, encoding="utf-8", **kwargs):
if fd is None:
with open(filename, encoding=encoding, **kwargs) as fileobj:
yield fileobj
else:
yield fd
class Control:
"""Represents a debian/control file"""
def __init__(self, filename, fd=None, use_rts_parser=None):
assert fd is not None or os.path.isfile(filename), f"{filename} does not exist."
self.filename = filename
self._is_roundtrip_safe = use_rts_parser
self.strip_trailing_whitespace_on_save = False
self.had_parse_errors = False
if self._is_roundtrip_safe:
# Note: wrap-and-sort does not trigger this code path without python-debian
# 0.1.44 due to the lack of formatter support (that we are not willing to
# re-implement ourselves). However, the 0.1.43 version is correct for the
# Control class itself and is left as-is for non-"wrap-and-sort" consumers
# (if any)
if not HAS_RTS_PARSER:
raise ValueError(
"The use_rts_parser option requires python-debian 0.1.43 or later"
)
# We allow parse errors in control-like files such as control.in as people
# might use template languages or placeholders in them. When there are
# parse errors, we cannot provide all features. However, most of them
# still work.
allow_parse_errors = (
not filename.endswith("/control") and filename != "control"
)
with _open(filename, fd=fd, encoding="utf8") as sequence:
self._deb822_file = parse_deb822_file(
sequence, accept_files_with_error_tokens=allow_parse_errors
)
self.paragraphs = list(self._deb822_file)
self.had_parse_errors = bool(self._deb822_file.find_first_error_element())
else:
self._deb822_file = None
self.paragraphs = []
with _open(filename, fd=fd, encoding="utf8") as sequence:
for paragraph in debian.deb822.Deb822.iter_paragraphs(sequence):
self.paragraphs.append(paragraph)
@property
def is_roundtrip_safe(self):
return self._is_roundtrip_safe
def get_maintainer(self):
"""Returns the value of the Maintainer field."""
return self.paragraphs[0].get("Maintainer")
def get_original_maintainer(self):
"""Returns the value of the XSBC-Original-Maintainer field."""
return self.paragraphs[0].get("XSBC-Original-Maintainer")
@property
def uses_different_style_tool(self):
return self.paragraphs[0].get("X-Style")
def dump(self):
if self.is_roundtrip_safe:
content = self._dump_rts_file()
else:
content = "\n".join(x.dump() for x in self.paragraphs)
if self.strip_trailing_whitespace_on_save:
content = "\n".join(x.rstrip() for x in content.splitlines()) + "\n"
return content
def _dump_rts_file(self):
# Use a custom dump of the RTS parser in order to:
# 1) support sorting of paragraphs
# 2) normalize whitespace between paragraphs
#
# Ideally, there would be a simpler way to do this - but for now, this is
# the best the RTS parser can offer. (Without the above constraints, we
# could just have used `self._deb822_file.dump()`)
paragraph_index = 0
new_content = ""
pending_newline = False
for part in self._deb822_file.iter_parts():
if isinstance(part, Deb822ParagraphElement):
part_content = self.paragraphs[paragraph_index].dump()
paragraph_index += 1
elif isinstance(part, Deb822Token) and part.is_whitespace:
# Normalize empty lines between paragraphs to a single newline.
#
# Note we do this unconditionally of
# strip_trailing_whitespace_on_save because preserving whitespace
# between paragraphs while reordering them produce funky results.
pending_newline = True
continue
else:
part_content = part.convert_to_text()
if pending_newline:
pending_newline = False
new_content += "\n"
new_content += part_content
return new_content
def save(self, filename=None):
"""Saves the control file."""
if filename:
self.filename = filename
content = self.dump()
with open(self.filename, "wb") as control_file:
control_file.write(content.encode("utf-8"))
def set_maintainer(self, maintainer):
"""Sets the value of the Maintainer field."""
self.paragraphs[0]["Maintainer"] = maintainer
def set_original_maintainer(self, original_maintainer):
"""Sets the value of the XSBC-Original-Maintainer field."""
if "XSBC-Original-Maintainer" in self.paragraphs[0]:
self.paragraphs[0]["XSBC-Original-Maintainer"] = original_maintainer
else:
_insert_after(
self.paragraphs[0],
"Maintainer",
"XSBC-Original-Maintainer",
original_maintainer,
)

View file

@ -0,0 +1,75 @@
# logger.py - A simple logging helper class
#
# Copyright (C) 2010, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software
# for any purpose with or without fee is hereby granted, provided
# that the above copyright notice and this permission notice appear
# in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import os
import sys
def escape_arg(arg):
"""Shell-escape arg, if necessary.
Fairly simplistic, doesn't escape anything except whitespace.
"""
if " " not in arg:
return arg
return '"%s"' % arg.replace("\\", r"\\").replace('"', r"\"")
class Logger:
script_name = os.path.basename(sys.argv[0])
verbose = False
stdout = sys.stdout
stderr = sys.stderr
@classmethod
def _print(cls, format_, message, args=None, stderr=False):
if args:
message = message % args
stream = cls.stderr if stderr else cls.stdout
stream.write((format_ + "\n") % (cls.script_name, message))
@classmethod
def command(cls, cmd):
if cls.verbose:
cls._print("%s: I: %s", " ".join(escape_arg(arg) for arg in cmd))
@classmethod
def debug(cls, message, *args):
if cls.verbose:
cls._print("%s: D: %s", message, args, stderr=True)
@classmethod
def error(cls, message, *args):
cls._print("%s: Error: %s", message, args, stderr=True)
@classmethod
def warn(cls, message, *args):
cls._print("%s: Warning: %s", message, args, stderr=True)
@classmethod
def info(cls, message, *args):
if cls.verbose:
cls._print("%s: I: %s", message, args)
@classmethod
def normal(cls, message, *args):
cls._print("%s: %s", message, args)
@classmethod
def set_verbosity(cls, verbose):
cls.verbose = verbose

View file

@ -0,0 +1,54 @@
#!/usr/bin/python3
import sys
from urllib.parse import quote
def main():
required_args = {"TO", "SUBJECT", "BODY"}
allowed_args = required_args | {"BCC", "CC"}
if len(sys.argv) < 4:
print("Usage: python3 -m devscripts.mailto KEY=<VALUE> ...")
print("Required keys: " + ", ".join(sorted(required_args)))
print("Supported keys: " + ", ".join(sorted(allowed_args)))
print()
print('The value can start with "@" to read the value from a file.')
sys.exit(1)
params = {}
for arg in sys.argv[1:]:
try:
name, value = arg.split("=")
except ValueError:
error("All arguments must be K=V pairs")
else:
if name not in allowed_args:
error(
"Unsupported key "
+ name
+ ": Must be one of: "
+ str(sorted(allowed_args))
)
if value.startswith("@"):
with open(value[1:], "rt", encoding="utf-8") as fd:
value = fd.read()
params[name] = quote(value)
if (params.keys() & required_args) < required_args:
error("The following keys must be given: " + str(sorted(required_args)))
to_part = params["TO"]
del params["TO"]
params_part = "&".join(k.lower() + "=" + v for k, v in params.items())
url = "mailto:" + to_part + "?" + params_part
print(url)
sys.exit(0)
def error(msg):
print("error: " + msg, file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

235
scripts/devscripts/proxy.py Normal file
View file

@ -0,0 +1,235 @@
# SPDX-FileCopyrightText: 2024 Johannes Schauer Marin Rodrigues <josch@debian.org>
# SPDX-License-Identifier: MIT
import http.server
import logging
import os
import pathlib
import shutil
import tempfile
import threading
import urllib
from functools import partial
from http import HTTPStatus
# we use a http proxy for two reasons
# 1. it allows us to cache package data locally which is useful even for
# single runs because temporally close snapshot timestamps share packages
# and thus we reduce the load on snapshot.d.o which is also useful because
# 2. snapshot.d.o requires manual bandwidth throttling or else it will cut
# our TCP connection. Instead of using Acquire::http::Dl-Limit as an apt
# option we use a proxy to only throttle on the initial download and then
# serve the data with full speed once we have it locally
#
# We use SimpleHTTPRequestHandler over BaseHTTPRequestHandler for its directory
# member. We disable its other features, namely do_HEAD
class Proxy(http.server.SimpleHTTPRequestHandler):
def do_HEAD(self):
raise NotImplementedError
# no idea how to split this function into parts without making it
# unreadable
def do_GET(self):
assert int(self.headers.get("Content-Length", 0)) == 0
assert self.headers["Host"]
pathprefix = "http://" + self.headers["Host"] + "/"
assert self.path.startswith(pathprefix)
sanitizedpath = urllib.parse.unquote(self.path.removeprefix(pathprefix))
# check validity and extract the timestamp
try:
chunk1, chunk2, timestamp, _ = sanitizedpath.split("/", 3)
except ValueError:
logging.error("don't know how to handle this request: %s", self.path)
self.send_error(HTTPStatus.BAD_REQUEST, f"Bad request path ({self.path})")
return
if ["archive", "debian"] != [chunk1, chunk2]:
logging.error("don't know how to handle this request: %s", self.path)
self.send_error(HTTPStatus.BAD_REQUEST, f"Bad request path ({self.path})")
return
# make sure the pool directory is symlinked to the global pool
linkname = os.path.join(self.directory, chunk1, chunk2, timestamp, "pool")
if not os.path.exists(linkname):
os.makedirs(
os.path.join(self.directory, chunk1, chunk2, timestamp), exist_ok=True
)
try:
os.symlink("../../../pool", linkname)
except FileExistsError:
pass
cachedir = pathlib.Path(self.directory)
path = cachedir / sanitizedpath
# just send back to client
if path.exists() and path.stat().st_size > 0:
self.wfile.write(b"HTTP/1.1 200 OK\r\n")
self.send_header("Content-Length", path.stat().st_size)
self.end_headers()
with path.open(mode="rb") as new:
while True:
buf = new.read(64 * 1024) # same as shutil uses
if not buf:
break
self.wfile.write(buf)
self.wfile.flush()
return
self.do_download(path)
# pylint: disable=too-many-branches,too-many-statements
def do_download(self, path):
# download fresh copy
todownload = downloaded_bytes = 0
partial_size = None
# The PID is part of the name of the temporary file. That way, multiple
# concurrent processes can write out partial files without conflicting
# with each other and while still maintaining reproducible paths
# between individual calls of do_download() by the same process.
tmppath = path.with_suffix(f".{os.getpid()}.part")
if self.headers.get("Range"):
assert tmppath.is_file()
assert self.headers["Range"].startswith("bytes=")
assert self.headers["Range"].endswith("-")
reqrange = int(
self.headers["Range"].removeprefix("bytes=").removesuffix("-")
)
assert reqrange <= tmppath.stat().st_size
partial_size = reqrange
else:
tmppath.parent.mkdir(parents=True, exist_ok=True)
conn = http.client.HTTPConnection(self.headers["Host"], timeout=30)
conn.request("GET", self.path, None, dict(self.headers))
try:
res = conn.getresponse()
except TimeoutError:
try:
self.send_error(504) # Gateway Timeout
except BrokenPipeError:
pass
return
if res.status == 302:
# clean up connection so it can be reused for the 302 redirect
res.read()
res.close()
newpath = res.getheader("Location")
assert newpath.startswith("/file/"), newpath
conn.request("GET", newpath, None, dict(self.headers))
try:
res = conn.getresponse()
except TimeoutError:
try:
self.send_error(504) # Gateway Timeout
except BrokenPipeError:
pass
return
if partial_size is not None:
if res.status != 206:
try:
self.send_error(res.status)
except BrokenPipeError:
pass
return
self.wfile.write(b"HTTP/1.1 206 Partial Content\r\n")
logging.info("proxy: resuming download from byte %d", partial_size)
else:
if res.status != 200:
try:
self.send_error(res.status)
except BrokenPipeError:
pass
return
self.wfile.write(b"HTTP/1.1 200 OK\r\n")
todownload = int(res.getheader("Content-Length"))
for key, value in res.getheaders():
# do not allow a persistent connection
if key == "connection":
continue
self.send_header(key, value)
self.end_headers()
if partial_size is not None:
total_size = todownload + partial_size
assert (
res.getheader("Content-Range")
== f"bytes {partial_size}-{total_size - 1}/{total_size}"
), (
res.getheader("Content-Range"),
f"bytes {partial_size}-{total_size - 1}/{total_size}",
)
downloaded_bytes = 0
with tmppath.open(mode="ab") as file:
if partial_size is not None and file.tell() != partial_size:
file.seek(partial_size, os.SEEK_SET)
# we are not using shutil.copyfileobj() because we want to
# write to two file objects simultaneously and throttle the
# writing speed to 1024 kB/s
while True:
buf = res.read(64 * 1024) # same as shutil uses
if not buf:
break
downloaded_bytes += len(buf)
try:
self.wfile.write(buf)
except BrokenPipeError:
break
file.write(buf)
# now that snapshot.d.o is fixed, we do not need to throttle
# the download speed anymore
# sleep(0.5) # 128 kB/s
self.wfile.flush()
if todownload == downloaded_bytes and downloaded_bytes > 0:
tmppath.rename(path)
# pylint: disable=redefined-builtin
def log_message(self, format, *args):
pass
def setupcache(cache, port):
if cache:
cachedir = cache
for path in pathlib.Path(cachedir).glob("**/*.part"):
# we are not deleting *.part files so that multiple processes can
# use the cache at the same time without having their *.part files
# deleted by another process
logging.warning(
"found partial file in cache, consider deleting it manually: %s", path
)
else:
cachedir = tempfile.mkdtemp(prefix="debbisect")
logging.info("using cache directory: %s", cachedir)
os.makedirs(cachedir + "/pool", exist_ok=True)
# we are not using a ThreadedHTTPServer because
# - additional complexity needed if one download created a .part file
# then apt stops reading while we still try to read from snapshot and
# apt retries the same download trying to write to the same .part file
# opened in another threat
# - snapshot.d.o really doesn't like fast downloads, so we do it serially
httpd = http.server.HTTPServer(
server_address=("127.0.0.1", port),
RequestHandlerClass=partial(Proxy, directory=cachedir),
)
# run server in a new thread
server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.daemon = True
# start thread
server_thread.start()
# retrieve port (in case it was generated automatically)
_, port = httpd.server_address
def teardown():
httpd.shutdown()
httpd.server_close()
server_thread.join()
if not cache:
# this should be a temporary directory but lets still be super
# careful
if os.path.exists(cachedir + "/pool"):
shutil.rmtree(cachedir + "/pool")
if os.path.exists(cachedir + "/archive"):
shutil.rmtree(cachedir + "/archive")
os.rmdir(cachedir)
return port, teardown

View file

@ -0,0 +1,72 @@
# Copyright (C) 2017-2021, Benjamin Drung <benjamin.drung@ionos.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""Helper functions for testing."""
import inspect
import os
import unittest
# This list is used by scripts/setup.py to populate the scripts argument
# for setup(). As such, this list governs what setup() will end up installing
# into /usr/bin.
SCRIPTS = [
"deb-check-file-conflicts",
"deb-janitor",
"debbisect",
"debdiff-apply",
"debftbfs",
"debootsnap",
"reproducible-check",
"sadt",
"suspicious-source",
"wrap-and-sort",
]
def get_source_files() -> list[str]:
"""Return a list of sources files/directories (to check with flake8/pylint)."""
modules = ["devscripts"]
py_files = ["setup.py"]
files = []
for code_file in SCRIPTS + modules + py_files:
is_script = code_file in SCRIPTS
if not os.path.exists(code_file): # pragma: no cover
# The alternative path is needed for Debian's pybuild
alternative = os.path.join(os.environ.get("OLDPWD", ""), code_file)
code_file = alternative if os.path.exists(alternative) else code_file
if is_script:
with open(code_file, "rb") as script_file:
shebang = script_file.readline().decode("utf-8")
if "python" in shebang:
files.append(code_file)
else:
files.append(code_file)
return files
def unittest_verbosity() -> int:
"""
Return the verbosity setting of the currently running unittest.
If no test is running, return 0.
"""
frame = inspect.currentframe()
while frame:
self = frame.f_locals.get("self")
if isinstance(self, unittest.TestProgram):
return self.verbosity
frame = frame.f_back
return 0 # pragma: no cover

View file

@ -0,0 +1,68 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=apt_pkg
# Pickle collected data for later comparisons.
persistent=no
[MESSAGES CONTROL]
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=fixme,locally-disabled,missing-docstring
[REPORTS]
# Tells whether to display a full report or only the messages
reports=no
[TYPECHECK]
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=magic
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=88
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
[BASIC]
# Allow variables called e, f, lp
good-names=i,j,k,ex,Run,_,e,f,lp,fd,fp,ok
[SIMILARITIES]
# Imports are removed from the similarity computation
ignore-imports=yes
# Minimum lines number of a similarity.
min-similarity-lines=5
[DESIGN]
# Maximum number of arguments per function
max-args=10
max-positional-arguments=10

View file

@ -0,0 +1,50 @@
# Copyright (C) 2021, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
"""Run black code formatter in check mode."""
import subprocess
import sys
import unittest
import black
from . import get_source_files, unittest_verbosity
class BlackTestCase(unittest.TestCase):
"""
This unittest class provides a test that runs the black code
formatter in check mode on the Python source code. The list of
source files is provided by the get_source_files() function.
"""
def test_black(self) -> None:
"""Test: Run black code formatter on Python source code."""
if int(black.__version__.split(".", 1)[0]) < 24:
self.skipTest("black >= 24 needed")
cmd = ["black", "--check", "--diff"] + get_source_files()
if unittest_verbosity() >= 2:
sys.stderr.write(f"Running following command:\n{' '.join(cmd)}\n")
process = subprocess.run(cmd, capture_output=True, check=False, text=True)
if process.returncode == 1: # pragma: no cover
self.fail(
f"black found code that needs reformatting:\n{process.stdout.strip()}"
)
if process.returncode != 0: # pragma: no cover
self.fail(
f"black exited with code {process.returncode}:\n"
f"{process.stdout.strip()}"
)

View file

@ -0,0 +1,280 @@
# Copyright (C) 2022, Niels Thykier <niels@thykier.net>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""test_control.py - Run unit tests for the Control module"""
import textwrap
try:
from debian._deb822_repro.formatter import (
COMMA_SEPARATOR_FT,
FormatterContentToken,
format_field,
)
except ImportError as e:
print(e)
COMMA_SEPARATOR = object()
FormatterContentToken = object()
def format_field(formatter, field_name, separator_token, token_iter):
raise AssertionError("Test should have been skipped!")
from devscripts.control import (
HAS_FULL_RTS_FORMATTING,
HAS_RTS_PARSER,
Control,
wrap_and_sort_formatter,
)
from devscripts.test import unittest
def _dedent(text):
"""Dedent and remove "EOL" line markers
Removes which are used as "EOL" markers. The EOL markers helps humans understand
that there is trailing whitespace (and it is significant) but also stops "helpful"
editors from pruning it away and thereby ruining the text.
"""
return textwrap.dedent(text).replace("", "")
def _prune_trailing_whitespace(text):
return "\n".join(x.rstrip() for x in text.splitlines()) + (
"\n" if text.endswith("\n") else ""
)
class ControlTestCase(unittest.TestCase):
@unittest.skipIf(not HAS_RTS_PARSER, "Requires a newer version of python-debian")
def test_rts_parsing(self):
orig_content = _dedent(
"""\
Source: devscripts
Maintainer: Jane Doe <jane.doe@debian.org>
# Some comment about Build-Depends: ¶
Build-Depends: foo,
# We need bar (>= 1.2~) because of reason ¶
bar (>=1.2~)
Package: devscripts
Architecture: arm64
linux-any
# Some comment describing why hurd-i386 would work while hurd-amd64 did not¶
hurd-i386
# This should be the "last" package after sorting¶
Package: z-pkg
Architecture: any
# Random comment here¶
# This should be the second one with -kb and the first with -b¶
Package: a-pkg
Architecture: any
"""
)
# "No change" here being just immediately dumping the content again. This will
# only prune empty lines (we do not preserve these in wrap-and-sort).
no_change_dump_content = _dedent(
"""\
Source: devscripts
Maintainer: Jane Doe <jane.doe@debian.org>
# Some comment about Build-Depends: ¶
Build-Depends: foo,
# We need bar (>= 1.2~) because of reason ¶
bar (>=1.2~)
Package: devscripts
Architecture: arm64
linux-any
# Some comment describing why hurd-i386 would work while hurd-amd64 did not¶
hurd-i386
# This should be the "last" package after sorting¶
Package: z-pkg
Architecture: any
# Random comment here¶
# This should be the second one with -kb and the first with -b¶
Package: a-pkg
Architecture: any
"""
)
last_paragraph_swap_no_trailing_space = _dedent(
"""\
Source: devscripts
Maintainer: Jane Doe <jane.doe@debian.org>
# Some comment about Build-Depends:¶
Build-Depends: foo,
# We need bar (>= 1.2~) because of reason¶
bar (>=1.2~)
Package: devscripts
Architecture: arm64
linux-any
# Some comment describing why hurd-i386 would work while hurd-amd64 did not¶
hurd-i386
# This should be the second one with -kb and the first with -b¶
Package: a-pkg
Architecture: any
# Random comment here¶
# This should be the "last" package after sorting¶
Package: z-pkg
Architecture: any
"""
)
control = Control(
"debian/control", fd=orig_content.splitlines(True), use_rts_parser=True
)
self.assertEqual(control.dump(), no_change_dump_content)
control.strip_trailing_whitespace_on_save = True
stripped_space = _prune_trailing_whitespace(no_change_dump_content)
self.assertNotEqual(stripped_space, no_change_dump_content)
self.assertEqual(control.dump(), stripped_space)
control.paragraphs[-2], control.paragraphs[-1] = (
control.paragraphs[-1],
control.paragraphs[-2],
)
self.assertEqual(control.dump(), last_paragraph_swap_no_trailing_space)
@unittest.skipIf(
not HAS_FULL_RTS_FORMATTING, "Requires a newer version of python-debian"
)
def test_rts_formatter(self):
# Note that we skip whitespace and separator tokens because:
# 1) The underlying formatters ignores them anyway, so they do not affect
# the outcome
# 2) It makes the test easier to understand
tokens_with_comment = [
FormatterContentToken.value_token("foo"),
FormatterContentToken.comment_token("# some comment about bar\n"),
FormatterContentToken.value_token("bar"),
]
tokens_without_comment = [
FormatterContentToken.value_token("foo"),
FormatterContentToken.value_token("bar"),
]
tokens_very_long_content = [
FormatterContentToken.value_token("foo"),
FormatterContentToken.value_token("bar"),
FormatterContentToken.value_token("some-very-long-token"),
FormatterContentToken.value_token("this-should-trigger-a-wrap"),
FormatterContentToken.value_token("with line length 20"),
FormatterContentToken.value_token(
"and (also) show we do not mash up spaces"
),
FormatterContentToken.value_token("inside value tokens"),
]
tokens_starting_comment = [
FormatterContentToken.comment_token("# some comment about foo\n"),
FormatterContentToken.value_token("foo"),
FormatterContentToken.value_token("bar"),
]
formatter_stl = wrap_and_sort_formatter(
1, # -s
immediate_empty_line=True, # -s
trailing_separator=True, # -t
max_line_length_one_liner=20, # --max-line-length
)
formatter_sl = wrap_and_sort_formatter(
1, # -s
immediate_empty_line=True, # -s
trailing_separator=False, # No -t
max_line_length_one_liner=20, # --max-line-length
)
actual = format_field(
formatter_stl, "Depends", COMMA_SEPARATOR_FT, tokens_without_comment
)
# Without comments, format this as one line
expected = textwrap.dedent(
"""\
Depends: foo, bar,
"""
)
self.assertEqual(actual, expected)
# With comments, we degenerate into "wrap_always" mode (for simplicity)
actual = format_field(
formatter_stl, "Depends", COMMA_SEPARATOR_FT, tokens_with_comment
)
expected = textwrap.dedent(
"""\
Depends:
foo,
# some comment about bar
bar,
"""
)
self.assertEqual(actual, expected)
# Starting with a comment should also work
actual = format_field(
formatter_stl, "Depends", COMMA_SEPARATOR_FT, tokens_starting_comment
)
expected = textwrap.dedent(
"""\
Depends:
# some comment about foo
foo,
bar,
"""
)
self.assertEqual(actual, expected)
# Without trailing comma
actual = format_field(
formatter_sl, "Depends", COMMA_SEPARATOR_FT, tokens_without_comment
)
expected = textwrap.dedent(
"""\
Depends: foo, bar
"""
)
self.assertEqual(actual, expected)
# Triggering a wrap
actual = format_field(
formatter_sl, "Depends", COMMA_SEPARATOR_FT, tokens_very_long_content
)
expected = textwrap.dedent(
"""\
Depends:
foo,
bar,
some-very-long-token,
this-should-trigger-a-wrap,
with line length 20,
and (also) show we do not mash up spaces,
inside value tokens
"""
)
self.assertEqual(actual, expected)

View file

@ -0,0 +1,56 @@
# Copyright (C) 2023, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
"""Test debootsnap script."""
import contextlib
import io
import tempfile
import unittest
import unittest.mock
from debootsnap import main, parse_pkgs
class TestDebootsnap(unittest.TestCase):
"""Test debootsnap script."""
@unittest.mock.patch("shutil.which")
def test_missing_tools(self, which_mock) -> None:
"""Test debootsnap fails cleanly if required binaries are missing."""
which_mock.return_value = None
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
with self.assertRaisesRegex(SystemExit, "1"):
main(["--packages=pkg1:arch=ver1", "chroot.tar"])
self.assertEqual(
stderr.getvalue(), "equivs-build is required but not installed\n"
)
which_mock.assert_called_once_with("equivs-build")
def test_parse_pkgs_from_file(self) -> None:
"""Test parse_pkgs() for a given file name."""
with tempfile.NamedTemporaryFile(mode="w", prefix="devscripts-") as pkgfile:
pkgfile.write("pkg1:arch=ver1\npkg2:arch=ver2\n")
pkgfile.flush()
pkgs = parse_pkgs(pkgfile.name)
self.assertEqual(pkgs, [[("pkg1", "arch", "ver1"), ("pkg2", "arch", "ver2")]])
def test_parse_pkgs_missing_file(self) -> None:
"""Test parse_pkgs() for a missing file name."""
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
with self.assertRaisesRegex(SystemExit, "1"):
parse_pkgs("/non-existing/pkgfile")
self.assertEqual(stderr.getvalue(), "/non-existing/pkgfile does not exist\n")

View file

@ -0,0 +1,59 @@
# Copyright (C) 2017-2018, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""Run flake8 check."""
import subprocess
import sys
import unittest
from . import get_source_files, unittest_verbosity
class Flake8TestCase(unittest.TestCase):
"""
This unittest class provides a test that runs the flake8 code
checker (which combines pycodestyle and pyflakes) on the Python
source code. The list of source files is provided by the
get_source_files() function.
"""
def test_flake8(self) -> None:
"""Test: Run flake8 on Python source code."""
cmd = [
sys.executable,
"-m",
"flake8",
"--ignore=E203,W503",
"--max-line-length=88",
] + get_source_files()
if unittest_verbosity() >= 2:
sys.stderr.write(f"Running following command:\n{' '.join(cmd)}\n")
process = subprocess.run(cmd, capture_output=True, check=False, text=True)
if process.returncode != 0: # pragma: no cover
msgs = []
if process.stderr:
msgs.append(
f"flake8 exited with code {process.returncode} and has"
f" unexpected output on stderr:\n{process.stderr.rstrip()}"
)
if process.stdout:
msgs.append(f"flake8 found issues:\n{process.stdout.rstrip()}")
if not msgs:
msgs.append(
f"flake8 exited with code {process.returncode} "
"and has no output on stdout or stderr."
)
self.fail("\n".join(msgs))

View file

@ -0,0 +1,84 @@
# test_help.py - Ensure scripts can run --help.
#
# Copyright (C) 2010, Stefano Rivera <stefanor@ubuntu.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
import fcntl
import os
import select
import signal
import subprocess
import time
import unittest
from . import SCRIPTS
TIMEOUT = 5
def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
"Give HelpTestCase a chance to populate before loading its test cases"
suite = unittest.TestSuite()
HelpTestCase.populate()
suite.addTests(loader.loadTestsFromTestCase(HelpTestCase))
return suite
class HelpTestCase(unittest.TestCase):
@classmethod
def populate(cls):
for script in SCRIPTS:
setattr(cls, "test_" + script, cls.make_help_tester(script))
@classmethod
def make_help_tester(cls, script):
def tester(self):
with subprocess.Popen(
["./" + script, "--help"],
close_fds=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as process:
started = time.time()
out = []
fds = [process.stdout.fileno(), process.stderr.fileno()]
for fd in fds:
fcntl.fcntl(
fd,
fcntl.F_SETFL,
fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK,
)
while time.time() - started < TIMEOUT:
for fd in select.select(fds, [], fds, TIMEOUT)[0]:
out.append(os.read(fd, 1024))
if process.poll() is not None:
break
if process.poll() is None:
os.kill(process.pid, signal.SIGTERM)
time.sleep(1)
if process.poll() is None:
os.kill(process.pid, signal.SIGKILL)
self.assertEqual(
process.poll(),
0,
f"{script} failed to return usage within {TIMEOUT} seconds.\n"
f"Output:\n{b''.join(out)}",
)
return tester

View file

@ -0,0 +1,42 @@
# Copyright (C) 2021, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
"""Run isort to check if Python import definitions are sorted."""
import subprocess
import sys
import unittest
from . import get_source_files, unittest_verbosity
class IsortTestCase(unittest.TestCase):
"""
This unittest class provides a test that runs isort to check if
Python import definitions are sorted. The list of source files
is provided by the get_source_files() function.
"""
def test_isort(self) -> None:
"""Test: Run isort on Python source code."""
cmd = ["isort", "--check-only", "--diff"] + get_source_files()
if unittest_verbosity() >= 2:
sys.stderr.write(f"Running following command:\n{' '.join(cmd)}\n")
process = subprocess.run(cmd, capture_output=True, check=False, text=True)
if process.returncode != 0: # pragma: no cover
self.fail(
f"isort found unsorted Python import definitions:\n"
f"{process.stdout.strip()}"
)

View file

@ -0,0 +1,57 @@
# test_logger.py - Test devscripts.logger.Logger.
#
# Copyright (C) 2012, Stefano Rivera <stefanor@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
import io
import sys
from devscripts.logger import Logger
from devscripts.test import unittest
class LoggerTestCase(unittest.TestCase):
def setUp(self):
Logger.stdout = io.StringIO()
Logger.stderr = io.StringIO()
self._script_name = Logger.script_name
Logger.script_name = "test"
self._verbose = Logger.verbose
def tearDown(self):
Logger.stdout = sys.stdout
Logger.stderr = sys.stderr
Logger.script_name = self._script_name
Logger.verbose = self._verbose
def test_command(self):
# pylint: disable=no-member
Logger.command(("ls", "a b"))
self.assertEqual(Logger.stdout.getvalue(), "")
Logger.set_verbosity(True)
Logger.command(("ls", "a b"))
self.assertEqual(Logger.stdout.getvalue(), 'test: I: ls "a b"\n')
self.assertEqual(Logger.stderr.getvalue(), "")
def test_no_args(self):
# pylint: disable=no-member
Logger.normal("hello %s")
self.assertEqual(Logger.stdout.getvalue(), "test: hello %s\n")
self.assertEqual(Logger.stderr.getvalue(), "")
def test_args(self):
# pylint: disable=no-member
Logger.normal("hello %s", "world")
self.assertEqual(Logger.stdout.getvalue(), "test: hello world\n")
self.assertEqual(Logger.stderr.getvalue(), "")

View file

@ -0,0 +1,82 @@
# Copyright (C) 2010, Stefano Rivera <stefanor@debian.org>
# Copyright (C) 2017-2018, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
"""Run pylint."""
import os
import re
import subprocess
import sys
import unittest
import pylint
from debian.debian_support import Version
from . import get_source_files, unittest_verbosity
CONFIG = os.path.join(os.path.dirname(__file__), "pylint.conf")
def check_pylint_version():
return Version(pylint.__version__) >= Version("2.11.1")
@unittest.skipIf(not check_pylint_version(), "pylint version not supported")
class PylintTestCase(unittest.TestCase):
"""
This unittest class provides a test that runs the pylint code check
on the Python source code. The list of source files is provided by
the get_source_files() function and pylint is purely configured via
a config file.
"""
def test_pylint(self) -> None:
"""Test: Run pylint on Python source code."""
cmd = ["pylint", "--rcfile=" + CONFIG, "--"] + get_source_files()
if unittest_verbosity() >= 2:
sys.stderr.write(f"Running following command:\n{' '.join(cmd)}\n")
process = subprocess.run(cmd, capture_output=True, check=False, text=True)
if process.returncode != 0: # pragma: no cover
# Strip trailing summary (introduced in pylint 1.7).
# This summary might look like:
#
# ------------------------------------
# Your code has been rated at 10.00/10
#
out = re.sub(
"^(-+|Your code has been rated at .*)$",
"",
process.stdout,
flags=re.MULTILINE,
).rstrip()
# Strip logging of used config file (introduced in pylint 1.8)
err = re.sub("^Using config file .*\n", "", process.stderr.rstrip())
msgs = []
if err:
msgs.append(
f"pylint exited with code {process.returncode} "
f"and has unexpected output on stderr:\n{err}"
)
if out:
msgs.append(f"pylint found issues:\n{out}")
if not msgs:
msgs.append(
f"pylint exited with code {process.returncode} "
"and has no output on stdout or stderr."
)
self.fail("\n".join(msgs))

View file

@ -0,0 +1,41 @@
# Copyright (C) 2023, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
"""Test suspicious-source script."""
import pathlib
import subprocess
import tempfile
import unittest
class TestSuspiciousSource(unittest.TestCase):
"""Test suspicious-source script."""
@staticmethod
def _run_suspicious_source(directory: str) -> str:
suspicious_source = subprocess.run(
["./suspicious-source", "-d", directory],
check=True,
stdout=subprocess.PIPE,
text=True,
)
return suspicious_source.stdout.strip()
def test_python_sript(self) -> None:
"""Test not complaining about Python code."""
with tempfile.TemporaryDirectory(prefix="devscripts-") as tmpdir:
python_file = pathlib.Path(tmpdir) / "example.py"
python_file.write_text("#!/usr/bin/python3\nprint('hello world')\n")
self.assertEqual(self._run_suspicious_source(tmpdir), "")

766
scripts/dget.pl Executable file
View file

@ -0,0 +1,766 @@
#!/usr/bin/perl
# vim: set ai shiftwidth=4 tabstop=4 expandtab:
# dget - Download Debian source and binary packages
# Copyright (C) 2005-2013 Christoph Berg <myon@debian.org>
# Modifications Copyright (C) 2005-06 Julian Gilbey <jdg@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# 2005-10-04 cb: initial release
# 2005-12-11 cb: -x option, update documentation
# 2005-12-31 cb: -b, -q options, use getopt
# 2006-01-10 cb: support new binnmu version scheme
# 2006-11-12 cb: also look in other places in the local filesystem (e.g. pbuilder result dir)
# Later modifications: see debian/changelog
use strict;
use warnings;
use Cwd qw(abs_path);
use IO::Dir;
use IO::File;
use Digest::MD5;
use Devscripts::Compression;
use Dpkg::Control;
use Dpkg::Path qw(find_command);
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use File::Basename;
# global variables
my $progname = basename($0, '.pl'); # the '.pl' is for when we're debugging
my $found_dsc;
my $wget;
my $opt;
my $backup_dir = "backup";
my @dget_path = ("/var/cache/apt/archives");
my $modified_conf_msg;
my $compression_re = compression_get_file_extension_regex();
# use curl if installed, wget otherwise
if (find_command('curl')) {
$wget = "curl";
} elsif (find_command('wget')) {
$wget = "wget";
} else {
die
"$progname: can't find either curl or wget; you need at least one of these\ninstalled to run me!\n";
}
# functions
sub usage {
print <<"EOT";
Usage: $progname [options] URL ...
$progname [options] [--all] package[=version] ...
Downloads Debian packages (source and binary) from the specified URLs (first form),
or using the mirror configured in /etc/apt/sources.list(.d) (second form).
Note that the second form is possible also with 'apt-get download' and 'apt-get
source', and thus the primary use case for dget is working with packages not in
any repository, for example when reviewing mentors.debian.net packages.
Multiple packages can be given as arguments and downloaded all at once.
-a, --all Package is a source package; download all binary packages
-b, --backup Move files that would be overwritten to ./backup
-q, --quiet Suppress wget/curl output
-d, --download-only
Do not extract downloaded source
-x, --extract Unpack downloaded source (default)
-u, --allow-unauthenticated
Do not attempt to verify source package signature
--build Build package with dpkg-buildpackage after download
--path DIR Check these directories in addition to the apt archive;
if DIR='' then clear current list (may be used multiple
times)
-k, --insecure Do not check SSL certificates when downloading
--no-cache Disable server-side HTTP cache
--no-conf Don\'t read devscripts config files;
must be the first option given
-h, --help This message
-V, --version Version information
Default settings modified by devscripts configuration files:
$modified_conf_msg
EOT
}
sub version {
print <<"EOF";
This is $progname, from the Debian devscripts package, version ###VERSION###
This code is copyright 2005-08 by Christoph Berg <myon\@debian.org>.
Modifications copyright 2005-06 by Julian Gilbey <jdg\@debian.org>.
All rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
EOF
}
sub wget {
my ($file, $url) = @_;
# schemes not supported by all backends
if ($url =~ m!^(file|copy):(.+)!) {
my $path = abs_path($2);
unless ($path) {
warn "$progname: unable to resolve full path for $2: $!\n";
return 1;
}
if ($1 eq "copy" or not link($path, $file)) {
system 'cp', '-aL', $path, $file;
return $? >> 8;
}
return;
}
my @cmd = ($wget);
# curl does not follow document moved headers, and does not exit
# with a non-zero error code by default if a document is not found
# also try to retain the mtime of the remote file
push @cmd, "-f", "-L", "-R" if $wget eq "curl";
push @cmd, ($wget eq "wget" ? "-nv" : ("-s", "-S")) if $opt->{'quiet'};
push @cmd, ($wget eq "wget" ? "--no-check-certificate" : "--insecure")
if $opt->{'insecure'};
push @cmd,
($wget eq "wget" ? "--no-cache" : ("--header", "Pragma: no-cache"))
if $opt->{'no-cache'};
push @cmd, ($wget eq "wget" ? "-O" : "-o");
system @cmd, $file, $url;
return $? >> 8;
}
sub backup_or_unlink {
my $file = shift;
return unless -e $file;
if ($opt->{'backup'}) {
unless (-d $backup_dir) {
mkdir $backup_dir or die "mkdir $backup_dir: $!";
}
rename $file, "$backup_dir/$file"
or die "rename $file $backup_dir/$file: $!";
} else {
unlink $file or die "unlink $file: $!";
}
}
# some files both are in .dsc and .changes, download only once
my %seen;
sub get_file {
my ($dir, $file, $md5sum) = @_;
return 1 if $seen{$file};
if ($md5sum eq "unlink") {
backup_or_unlink($file);
}
# check the existing file's md5sum
if (-e $file) {
my $md5 = Digest::MD5->new;
my $fh5 = new IO::File($file) or die "$file: $!";
my $md5sum_new = Digest::MD5->new->addfile($fh5)->hexdigest();
close $fh5;
if (not $md5sum or ($md5sum_new eq $md5sum)) {
print "$progname: using existing $file\n" unless $opt->{'quiet'};
} else {
print "$progname: removing $file (md5sum does not match)\n"
unless $opt->{'quiet'};
backup_or_unlink($file);
}
}
# look for the file in other local directories
unless (-e $file) {
foreach my $path (@dget_path) {
next unless -e "$path/$file";
my $fh5 = new IO::File("$path/$file") or next;
my $md5 = Digest::MD5->new;
my $md5sum_new = Digest::MD5->new->addfile($fh5)->hexdigest();
close $fh5;
if ($md5sum_new eq $md5sum) {
if (link "$path/$file", $file) {
print "$progname: using $path/$file (hardlink)\n"
unless $opt->{'quiet'};
} else {
print "$progname: using $path/$file (copy)\n"
unless $opt->{'quiet'};
system 'cp', '-aL', "$path/$file", $file;
}
last;
}
}
}
# finally get it from the web
unless (-e $file) {
print "$progname: retrieving $dir/$file\n" unless $opt->{'quiet'};
if (wget($file, "$dir/$file")) {
warn "$progname: $wget $file $dir/$file failed\n";
unlink $file;
}
}
# try apt-get if it is still not there
my $ext = $compression_re;
if (not -e $file
and $file =~ m!^([a-z0-9][a-z0-9.+-]+)_[^/]+\.(?:diff|tar)\.$ext$!) {
my @cmd = ('apt-get', 'source', '--print-uris', $1);
my $cmd = join ' ', @cmd;
open(my $apt, '-|', @cmd) or die "$cmd: $!";
while (<$apt>) {
if (/'(\S+)'\s+\S+\s+\d+\s+([\da-f]+)/i and $2 eq $md5sum) {
if (wget($file, $1)) {
warn "$progname: $wget $file $1 failed\n";
unlink $file;
}
}
}
close $apt;
}
# still not there, return
unless (-e $file) {
return 0;
}
$seen{$file} = 1;
if ($file =~ /\.(?:changes|dsc)$/) {
parse_file($dir, $file);
}
if ($file =~ /\.dsc$/) {
$found_dsc = $file;
}
return 1;
}
sub parse_file {
my ($dir, $file) = @_;
my $fh = new IO::File($file);
open $fh, $file or die "$file: $!";
while (<$fh>) {
if (/^ ([0-9a-f]{32}) (?:\S+ )*(\S+)$/) {
my ($_sum, $_file) = ($1, $2);
$_file !~ m,[/\x00],
or die "File name contains invalid characters: $_file";
get_file($dir, $_file, $_sum) or return;
}
}
close $fh;
}
sub quote_version {
my $version = shift;
$version = quotemeta($version);
$version =~ s/^([^:]+:)/(?:$1)?/; # Epochs are not part of the filename
$version
=~ s/-([^.-]+)$/-$1(?:\\+b\\d+|\.0\.\\d+)?/; # BinNMU: -x -> -x.0.1 -x+by
$version =~ s/-([^.-]+\.[^.-]+)$/-$1(?:\\+b\\d+|\.\\d+)?/
; # -x.y -> -x.y.1 -x.y+bz
return $version;
}
# This section implemented "apt-get download" and "apt-get source" probably
# before apt-get had the capabilities, and since then has become obsolete.
sub apt_get {
my ($package, $version) = @_;
my ($archpackage, $arch) = $package;
($package, $arch) = split(/:/, $package, 2);
my $qpackage = quotemeta($package);
my $qversion = quote_version($version) if $version;
my @hosts;
my $apt = IO::File->new("LC_ALL=C apt-cache policy $archpackage |")
or die "$!";
OUTER: while (<$apt>) {
if (not $version and /^ Candidate: (.+)/) {
$version = $1;
$qversion = quote_version($version);
}
if ($qversion and /^ [ *]{3} ($qversion) \d/) {
while (<$apt>) {
last OUTER unless /^ *(?:\d+) (\S+)/;
(my $host = $1) =~ s@/$@@;
next if $host eq '/var/lib/dpkg/status';
push @hosts, $host;
}
}
}
close $apt;
unless ($version) {
die "$progname: $archpackage has no installation candidate\n";
}
unless (@hosts) {
die
"$progname: no hostnames in apt-cache policy $archpackage for version $version found\n";
}
$apt = IO::File->new("LC_ALL=C apt-cache show $archpackage=$version |")
or die "$!";
my ($v, $p, $filename, $md5sum);
while (<$apt>) {
if (/^Package: $qpackage$/) {
$p = $package;
}
if (/^Version: $qversion$/) {
$v = $version;
}
if (/^Filename: (.*)/) {
$filename = $1;
}
if (/^MD5sum: (.*)/) {
$md5sum = $1;
}
if (/^Description:/) { # we assume this is the last field
if ($p and $v and $filename) {
last;
}
undef $p;
undef $v;
undef $filename;
undef $md5sum;
}
}
close $apt;
unless ($filename) {
die "$progname: no filename for $archpackage ($version) found\n";
}
# find deb lines matching the hosts in the policy output
my %repositories;
# the regexp within the map below can be removed and replaced with only the quotemeta statement once bug #154868 is fixed
my $host_re = '(?:' . (
join '|',
map {
my $host = quotemeta;
$host =~ s@^(\w+\\:\\/\\/[^:/]+)\\/@$1(?::[0-9]+)?\\/@;
$host;
} @hosts
) . ')';
my @sources;
if (-f "/etc/apt/sources.list") {
push @sources, "/etc/apt/sources.list";
}
my %dir;
tie %dir, "IO::Dir", "/etc/apt/sources.list.d";
foreach (keys %dir) {
next unless /\.list$|\.sources$/;
push @sources, "/etc/apt/sources.list.d/$_";
}
$apt
= IO::File->new(
"LC_ALL=C apt-get indextargets 'Identifier: Packages' --format '\$(BASE_URI)' |"
) or die "$!";
while (<$apt>) {
if (/^($host_re)/) {
$repositories{$1} = 1;
}
}
close $apt;
unless (%repositories) {
die "no repository found in /etc/apt/sources.list or sources.list.d";
}
# try each repository in turn
foreach my $repository (keys %repositories) {
my ($dir, $file) = ($repository, $filename);
if ($filename =~ /(.*)\/([^\/]*)$/) {
($dir, $file) = ("$repository/$1", $2);
}
get_file($dir, $file, $md5sum) and return;
}
exit 1;
}
# main program
# Now start by reading configuration files and then command line
# The next stuff is boilerplate
my ($dget_path, $dget_unpack, $dget_verify);
if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
$modified_conf_msg = " (no configuration files read)";
shift;
} else {
my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
my %config_vars = (
'DGET_PATH' => '',
'DGET_UNPACK' => 'yes',
'DGET_VERIFY' => 'yes',
);
my %config_default = %config_vars;
my $shell_cmd;
# Set defaults
foreach my $var (keys %config_vars) {
$shell_cmd .= "$var='$config_vars{$var}';\n";
}
$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
# Read back values
foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
my $shell_out = `/bin/bash -c '$shell_cmd'`;
@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
foreach my $var (sort keys %config_vars) {
if ($config_vars{$var} ne $config_default{$var}) {
$modified_conf_msg .= " $var=$config_vars{$var}\n";
}
}
$modified_conf_msg ||= " (none)\n";
chomp $modified_conf_msg;
$dget_path = $config_vars{'DGET_PATH'};
$dget_unpack = $config_vars{'DGET_UNPACK'} =~ /^y/i;
$dget_verify = $config_vars{'DGET_VERIFY'} =~ /^y/i;
}
# handle options
Getopt::Long::Configure('bundling');
GetOptions(
"a|all" => \$opt->{'all'},
"b|backup" => \$opt->{'backup'},
"q|quiet" => \$opt->{'quiet'},
"build" => \$opt->{'build'},
"d|download-only" => sub { $dget_unpack = 0 },
"x|extract" => sub { $dget_unpack = 1 },
"u|allow-unauthenticated" => sub { $dget_verify = 0 },
"k|insecure" => \$opt->{'insecure'},
"no-cache" => \$opt->{'no-cache'},
"noconf|no-conf" => \$opt->{'no-conf'},
"path=s" => sub {
if ($_[1] eq '') { $dget_path = ''; }
else { $dget_path .= ":$_[1]"; }
},
"h|help" => \$opt->{'help'},
"V|version" => \$opt->{'version'},
)
or die
"$progname: unrecognised option. Run $progname --help for more details.\n";
if ($opt->{'help'}) { usage(); exit 0; }
if ($opt->{'version'}) { version(); exit 0; }
if ($opt->{'no-conf'}) {
die
"$progname: --no-conf is only acceptable as the first command-line option!\n";
}
if ($dget_path) {
foreach my $p (split /:/, $dget_path) {
push @dget_path, $p if -d $p;
}
}
if (!@ARGV) {
die
"Usage: $progname [options] URL|package[=version]\nRun $progname --help for more details.\n";
}
# handle arguments
for my $arg (@ARGV) {
$found_dsc = "";
# case 1: URL
if ($arg
=~ /^((?:copy|file|ftp|gopher|http|rsh|rsync|scp|sftp|ssh|www).*)\/([^\/]+\.\w+)$/
) {
get_file($1, $2, "unlink") or exit 1;
if ($found_dsc) {
if ($dget_verify) { # We are duplicating work here a bit as
# dpkg-source -x will also verify signatures. Still, we
# also want to barf with -d, and on unsigned packages.
system 'dscverify', $found_dsc;
exit $? >> 8 if $? >> 8 != 0;
}
my @cmd = qw(dpkg-source -x);
push @cmd, '--no-check' unless $dget_verify;
if ($opt->{'build'}) {
my @output = `LC_ALL=C @cmd $found_dsc`;
my $rc = $?;
print @output unless $opt->{'quiet'};
exit $rc >> 8 if $rc >> 8 != 0;
foreach (@output) {
if (
/^.*dpkg-source:.* (?:.*info.*: )?extracting .* in (.*)/
) {
chdir $1;
exec 'dpkg-buildpackage', '-b', '-uc';
die "Unable to run dpkg-buildpackage: $!";
}
}
} elsif ($dget_unpack) {
system @cmd, $found_dsc;
exit $? >> 8 if $? >> 8 != 0;
}
}
# case 2a: --all srcpackage[=version]
} elsif ($opt->{'all'}
and $arg =~ /^([a-z0-9.+-:]{2,})(?:=([a-zA-Z0-9.:~+-]+))?$/) {
my ($source, $version, $arch) = ($1, $2);
($source, $arch) = split(/:/, $source, 2);
my $cmd = "apt-cache showsrc --only-source $source";
# unfortunately =version doesn't work here, and even if it did, was the
# user referring to the source version or the binary version? The code
# assumes binary version.
#$cmd .= "=$version" if ($version);
open my $showsrc, '-|', $cmd;
my $c = Dpkg::Control->new(type => CTRL_INDEX_SRC);
while ($c->parse($showsrc, $cmd)) {
if ($arch) {
my @packages = grep { $_ } split /\n/, $c->{'Package-List'};
# Find all packages whose architecture is either 'all', 'any',
# or the given architecture. The Package-List lines are
# $pkg $debtype $section $priority arch=$archlist
foreach my $package (@packages) {
$package =~ s/^\s*//;
my ($binary, $debtype, $section, $priority, $archs)
= split(/\s+/, $package, 5);
if ($archs =~ m/all/) {
eval { apt_get($binary, $version) } or print "$@";
} elsif ($archs =~ m/any|[=,]$arch/) {
eval { apt_get("$binary:$arch", $version) }
or print "$@";
}
}
} else {
my @packages = split /,\s*/, $c->{Binary};
foreach my $package (@packages) {
eval { apt_get($package, $version) } or print "$@";
}
}
last;
}
close $showsrc;
# case 2b: package[=version]
} elsif ($arg =~ /^([a-z0-9.+-:]{2,})(?:=([a-zA-Z0-9.:~+-]+))?$/) {
apt_get($1, $2);
} else {
usage();
}
}
=head1 NAME
dget - Download Debian source and binary packages
=head1 SYNOPSIS
=over
=item B<dget> [I<options>] I<URL> ...
=item B<dget> [I<options>] [B<--all>] I<package>[B<=>I<version>] ...
=back
=head1 DESCRIPTION
B<dget> downloads Debian packages. In the first form, B<dget> fetches
the requested URLs. If this is a .dsc or .changes file, then B<dget>
acts as a source-package aware form of B<wget>: it also fetches any
files referenced in the .dsc/.changes file. The downloaded source is
then checked with B<dscverify> and, if successful, unpacked by
B<dpkg-source>.
In the second form, B<dget> downloads a I<binary> package (i.e., a
I<.deb> file) from the Debian mirror configured in
/etc/apt/sources.list(.d). If a version number is specified, this version
of the package is requested. With B<--all>, the list of all binaries for the
source package I<package> is extracted from the output of
C<apt-cache showsrc package>.
In both cases dget is capable of getting several packages and/or URLs
at once.
(Note that I<.udeb> packages used by debian-installer are located in separate
packages files from I<.deb> packages. In order to use I<.udebs> with B<dget>,
you will need to have configured B<apt> to use a packages file for
I<component>/I<debian-installer>).
Before downloading files listed in .dsc and .changes files, and before
downloading binary packages, B<dget> checks to see whether any of
these files already exist. If they do, then their md5sums are
compared to avoid downloading them again unnecessarily. B<dget> also
looks for matching files in I</var/cache/apt/archives> and directories
given by the B<--path> option or specified in the configuration files
(see below). Finally, if downloading (.orig).tar.gz or .diff.gz files
fails, dget consults B<apt-get source --print-uris>. Download backends
used are B<curl> and B<wget>, looked for in that order.
B<dget> I<package> should be implemented in B<apt-get install -d>.
B<dget> was written to make it easier to retrieve source packages from the web
to sponsor uploads, and thus the primary use case is downloading binary and
source packages from a URL. For fetching packages from apt repositories it is
easier to simply run B<apt-get download> I<package> and B<apt-get source>
I<package> with optional B<--download-only> to not uncompress it with
B<dpkg-source> automatically, or I<package>=1.22-1 to define an exact version
instead of just the latest version.
=head1 OPTIONS
=over 4
=item B<-a>, B<--all>
Interpret I<package> as a source package name, and download all binaries as
found in the output of "apt-cache showsrc I<package>". If I<package> is
arch-qualified, then only binary packages which are "Arch: all", "Arch: any",
or "Arch: $arch" will be downloaded.
=item B<-b>, B<--backup>
Move files that would be overwritten to I<./backup>.
=item B<-q>, B<--quiet>
Suppress B<wget>/B<curl> non-error output.
=item B<-d>, B<--download-only>
Do not run B<dpkg-source -x> on the downloaded source package. This can
only be used with the first method of calling B<dget>.
=item B<-x>, B<--extract>
Run B<dpkg-source -x> on the downloaded source package to unpack it.
This option is the default and can only be used with the first method of
calling B<dget>.
=item B<-u>, B<--allow-unauthenticated>
Do not attempt to verify the integrity of downloaded source packages
using B<dscverify>.
=item B<--build>
Run B<dpkg-buildpackage -b -uc> on the downloaded source package.
=item B<--path> I<DIR>[B<:>I<DIR> ...]
In addition to I</var/cache/apt/archives>, B<dget> uses the
colon-separated list given as argument to B<--path> to find files with
a matching md5sum. For example: "--path
/srv/pbuilder/result:/home/cb/UploadQueue". If DIR is empty (i.e.,
"--path ''" is specified), then any previously listed directories
or directories specified in the configuration files will be ignored.
This option may be specified multiple times, and all of the
directories listed will be searched; hence, the above example could
have been written as: "--path /srv/pbuilder/result --path
/home/cb/UploadQueue".
=item B<-k>, B<--insecure>
Allow SSL connections to untrusted hosts.
=item B<--no-cache>
Bypass server-side HTTP caches by sending a B<Pragma: no-cache> header.
=item B<-h>, B<--help>
Show a help message.
=item B<-V>, B<--version>
Show version information.
=back
=head1 CONFIGURATION VARIABLES
The two configuration files F</etc/devscripts.conf> and
F<~/.devscripts> are sourced by a shell in that order to set
configuration variables. Command line options can be used to override
configuration file settings. Environment variable settings are
ignored for this purpose. The currently recognised variable is:
=over 4
=item B<DGET_PATH>
This can be set to a colon-separated list of directories in which to
search for files in addition to the default
I</var/cache/apt/archives>. It has the same effect as the B<--path>
command line option. It is not set by default.
=item B<DGET_UNPACK>
Set to 'no' to disable extracting downloaded source packages. Default
is 'yes'.
=item B<DGET_VERIFY>
Set to 'no' to disable checking signatures of downloaded source
packages. Default is 'yes'.
=back
=head1 EXAMPLES
Download all binary I<.deb> files for current and previous version of a package,
and compare them byte-for-byte with B<diffoscope>:
mkdir previous latest
(cd latest && dget --all mypackage=1.2-1)
(cd previous && dget --all mypackage) # download latest 1.2-2 in this example
diffoscope --html=diffoscope.html previous/ latest/
Download the source package of the current version in apt repository and the
to-be-reviewed new version at mentors.debian.net, and compare them with
B<debdiff>:
dget https://mentors.debian.net/debian/pool/main/m/mypackage/mypackage_1.2-3.dsc
apt-get source mypackage=1.2-2
debdiff --from mypackage_1.2-2.dsc --to mypackage_1.2-3.dsc
=head1 BUGS AND COMPATIBILITY
B<dget --all> I<srcpkg> should be implemented in B<apt-get download> I<srcpkg>
so B<apt-get> could download all binary packages based on source package name.
Before devscripts version 2.10.17, the default was not to extract the
downloaded source. Set DGET_UNPACK=no to revert to the old behaviour.
=head1 AUTHOR
This program is Copyright (C) 2005-2013 by Christoph Berg <myon@debian.org>.
Modifications are Copyright (C) 2005-06 by Julian Gilbey <jdg@debian.org>.
This program is licensed under the terms of the GPL, either version 2
of the License, or (at your option) any later version.
=head1 SEE ALSO
B<apt-get>(1), B<curl>(1), B<debcheckout>(1), B<debdiff>(1), B<dpkg-source>(1),
B<wget>(1)

50
scripts/diff2patches.1 Normal file
View file

@ -0,0 +1,50 @@
.TH "diff2patches" "1" "" "Raphael Geissert <atomo64@gmail.com>" ""
.SH "NAME"
.LP
diff2patches \- Extract non\-debian/ patches from .diff.gz files
.SH "SYNTAX"
.LP
\fBdiff2patches \fIfilename\fP
.br
\fBdiff2patches \-\-help\fR|\fB\-\-version\fP
.SH "DESCRIPTION"
.LP
Extracts patches from .diff.gz which apply to files outside the
\*(lqdebian/\*(rq directory scope. A patch is created for each modified file.
Each patch is named according to the path of the modified file, with \*(lq/\*(rq
replaced by \*(lq___\*(rq, and an extension of \*(lq.patch\*(rq.
.SH "OPTIONS"
.LP
.TP 4
\fB\fIfilename\fP\fR
Extract patches from \fB\fIfilename\fP\fR which apply outside the
\*(lqdebian/\*(rq directory.
.TP
\fB\-\-help\fR
Output help information and exit.
.TP
\fB\-\-version\fR
Output version information and exit.
.SH "FILES"
.TP
\fIdebian/control\fP
Existence of this file is tested before any patch is extracted.
.TP
\fIdebian/\fP
.TQ
\fIdebian/patches/\fP
Patches are extracted to one of these directories. \*(lqdebian/patches/\*(rq is
preferred, if it exists. If \fIDEB_PATCHES\fP is present in the environment,
it will override this behavior (see \*(lqENVIRONMENT VARIABLES\*(rq section
below).
.SH "ENVIRONMENT VARIABLES"
.TP
\fBDEB_PATCHES\fP
When defined and points to an existing directory, patches are extracted in that directory
and not under \*(lqdebian/\*(rq nor \*(lqdebian/patches/\*(rq.
.SH "SEE ALSO"
.LP
\fBcombinediff\fR(1)
.SH "AUTHOR"
.LP
Raphael Geissert <atomo64@gmail.com>

93
scripts/diff2patches.sh Executable file
View file

@ -0,0 +1,93 @@
#!/bin/bash
####################
# Copyright (C) 2007, 2008 by Raphael Geissert <atomo64@gmail.com>
#
# This file is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file If not, see <https://www.gnu.org/licenses/>.
#
# On Debian systems, the complete text of the GNU General
# Public License 3 can be found in '/usr/share/common-licenses/GPL-3'.
####################
set -e
PROGNAME=${0##*/}
usage() {
echo \
"Usage: $PROGNAME [options] FILE.diff.gz
Options:
--help Show this message
--version Show version and copyright information
debian/control must exist on the current path for this script to work
If debian/patches exists and is a directory, patches are extracted there,
otherwise they are extracted under debian/ (unless the environment variable
DEB_PATCHES is defined and points to a valid directory, in which case
patches are extracted there)."
}
version() {
echo \
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
This code is copyright 2007, 2008 by Raphael Geissert, all rights reserved.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 3 or later."
}
case "$1" in
--help) usage; exit 0 ;;
--version) version; exit 0 ;;
esac
if ! which lsdiff > /dev/null 2>&1; then
echo "lsdiff was not found in \$PATH, package patchutils probably not installed!"
exit 1
fi
diffgz="$1"
if [ ! -f "$diffgz" ]; then
[ -z "$diffgz" ] && diffgz="an unspecified .diff.gz"
echo "Couldn't find $diffgz, aborting!"
exit 1
fi
if [ -x /usr/bin/dh_testdir ]; then
/usr/bin/dh_testdir || exit 1
else
[ ! -f debian/control ] && echo "Couldn't find debian/control!" && exit 1
fi
if [ -z "$DEB_PATCHES" ] || [ ! -d "$DEB_PATCHES" ]; then
DEB_PATCHES=debian
[ -d debian/patches ] && DEB_PATCHES=debian/patches
else
DEB_PATCHES="$(readlink -f "$DEB_PATCHES")"
fi
echo "Patches will be extracted under $DEB_PATCHES/"
FILES=$(zcat "$diffgz" | lsdiff --strip 1 | grep -v ^debian/) || \
echo "$(basename "$diffgz") doesn't contain any patch outside debian/"
for file in $FILES; do
[ ! -z "$file" ] || continue
echo -n "Extracting $file..."
newFileName="$DEB_PATCHES/$(echo "$file" | sed 's#/#___#g').patch"
zcat "$diffgz" | filterdiff -i "$file" -p1 > "$newFileName"
echo "done"
done
exit

130
scripts/dpkg-depcheck.1 Normal file
View file

@ -0,0 +1,130 @@
.TH DPKG-DEPCHECK "1" "March 2002" "dpkg-depcheck" DEBIAN
.SH NAME
dpkg-depcheck \- determine packages used to execute a command
.SH SYNOPSIS
\fBdpkg-depcheck\fR [\fIoptions\fR] \fIcommand\fR
.SH DESCRIPTION
This program runs the specified command under \fBstrace\fR and then
determines and outputs the packages used in the process. The list can
be trimmed in various ways as described in the options below. A good
example of this program would be the command \fBdpkg-depcheck \-b
debian/rules build\fR, which would give a good first approximation to
the Build-Depends line needed by a Debian package. Note, however,
that this does \fInot\fR give any direct information on versions
required or architecture-specific packages.
.SH OPTIONS
.TP
.BR \-a ", " \-\-all
Report all packages used to run \fIcommand\fR. This is the default
behaviour. If used in conjunction with \fB\-b\fR, \fB\-d\fR or
\fB\-m\fR, gives additional information on those packages skipped by
these options.
.TP
.BR \-b ", " \-\-build-depends
Do not report any build-essential or essential packages used, or any
of their (direct or indirect) dependencies.
.TP
.BR \-d ", " \-\-ignore-dev-deps
Do not show packages used which are direct dependencies of \fI\-dev\fR
packages used. This implies \fB\-b\fR.
.TP
.BR \-m ", " \-\-min-deps
Output a minimal set of packages needed, taking into account direct
dependencies. Using \fB\-m\fR implies \fB\-d\fR and also \fB\-b\fR.
.TP
.BR \-C ", " \-\-C-locale
Run \fIcommand\fR with the C locale.
.TP
.BR \-\-no-C-locale
Don't change locale when running \fIcommand\fR.
.TP
.BR \-l ", " \-\-list-files
Also report the list of files used in each package.
.TP
.BR \-\-no-list-files
Do not report the files used in each package. Cancels a \fB\-l\fR
option.
.TP
\fB\-o\fR, \fB\-\-output=\fIFILE\fR
Output the package diagnostics to \fIFILE\fR instead of stdout.
.TP
\fB\-O\fR, \fB\-\-strace-output=\fIFILE\fR
Write the \fBstrace\fR output to \fIFILE\fR when tracing \fIcommand\fR
instead of using a temporary file.
.TP
\fB\-I\fR, \fB\-\-strace-input=\fIFILE\fR
Get \fBstrace\fR output from \fIFILE\fR instead of tracing
\fIcommand\fR; \fBstrace\fR must have be run with the \fB\-f \-q\fR
options for this to work.
.TP
\fB\-f\fR, \fB\-\-features=\fILIST\fR
Enable or disabled features given in the comma-separated \fILIST\fR as
follows. A feature is enabled with \fI+feature\fR or just
\fIfeature\fR and disabled with \fI\-feature\fR. The currently
recognised features are:
.PD 0
.RS
.TP
.B warn\-local
Warn if files in \fI/usr/local\fR or \fI/var/local\fR are used.
Enabled by default.
.TP
.B discard-check-version
Discards \fIexecve\fR when only a \fI\-\-version\fR argument is given
to the program; this works around some configure scripts that check
for binaries they don't actually use. Enabled by default.
.TP
.B trace-local
Also try to identify files which are accessed in \fI/usr/local\fR and
\fI/var/local\fR. Not usually very useful, as Debian does not place
files in these directories. Disabled by default.
.TP
.B catch-alternatives
Warn about access to files controlled by the Debian \fIalternatives\fR
mechanism. Enabled by default.
.TP
.B discard-sgml-catalogs
Discards access to SGML catalogs; some SGML tools read all the registered
catalogs at startup. Files matching the regexp /usr/share/sgml/.*\\.cat are
recognised as catalogs. Enabled by default.
.PD
.RE
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
.BR \-h ", " \-\-help
Display usage information and exit.
.TP
.BR \-v ", " \-\-version
Display version and copyright information and exit.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced in that order to set configuration
variables. Command line options can be used to override configuration
file settings. Environment variable settings are ignored for this
purpose. The currently recognised variable is:
.TP
.B DPKG_DEPCHECK_OPTIONS
These are options which are parsed before the command-line options.
For example,
.IP
DPKG_DEPCHECK_OPTIONS="\-b \-f-catch-alternatives"
.IP
which passes these options to \fBdpkg-depcheck\fR before any
command-line options are processed. You are advised not to try tricky
quoting, because of the vagaries of shell quoting!
.SH "SEE ALSO"
.BR dpkg (1),
.BR strace (1),
.BR devscripts.conf (5),
.BR update-alternatives (8)
.SH "COPYING"
Copyright 2001 Bill Allombert <ballombe@debian.org>.
Modifications copyright 2002,2003 Julian Gilbey <jdg@debian.org>.
\fBdpkg-depcheck\fR is free software, covered by the GNU General
Public License, version 2 or (at your option) any later version,
and you are welcome to change it and/or distribute copies of it under
certain conditions. There is absolutely no warranty for
\fBdpkg-depcheck\fR.

534
scripts/dpkg-depcheck.pl Executable file
View file

@ -0,0 +1,534 @@
#!/usr/bin/perl
# Copyright Bill Allombert <ballombe@debian.org> 2001.
# Modifications copyright 2002-2005 Julian Gilbey <jdg@debian.org>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use strict;
use warnings;
use 5.006_000; # our() commands
use Cwd;
use File::Basename;
use Getopt::Long;
use Devscripts::Set;
use Devscripts::Packages;
use Devscripts::PackageDeps;
# Function prototypes
sub process_features ($$);
sub getusedfiles (@);
sub filterfiles (@);
# Global options
our %opts;
# A list of files that do not belong to a Debian package but are known
# to never create a dependency
our @known_files = (
"/etc/ld.so.cache", "/etc/dpkg/shlibs.default",
"/etc/dpkg/dpkg.cfg", "/etc/devscripts.conf"
);
# This will be given information about features later on
our (%feature, %default_feature);
my $progname = basename($0);
my $modified_conf_msg;
sub usage () {
my @ed = ("disabled", "enabled");
print <<"EOF";
Usage:
$progname [options] <command>
Run <command> and then output packages used to do this.
Options:
Which packages to report:
-a, --all Report all packages used to run <command>
-b, --build-depends Do not report build-essential or essential packages
used or any of their (direct or indirect)
dependencies
-d, --ignore-dev-deps Do not show packages used which are direct
dependencies of -dev packages used
-m, --min-deps Output a minimal set of packages needed, taking
into account direct dependencies
-m implies -d and both imply -b; -a gives additional dependency information
if used in conjunction with -b, -d or -m
-C, --C-locale Run command with C locale
--no-C-locale Don\'t change locale
-l, --list-files Report list of files used in each package
--no-list-files Do not report list of files used in each package
-o, --output=FILE Output diagnostic to FILE instead of stdout
-O, --strace-output=FILE Write strace output to FILE when tracing <command>
instead of a temporary file
-I, --strace-input=FILE Get strace output from FILE instead of tracing
<command>; strace must be run with -f -q for this
to work
-f, --features=LIST Enable or disabled features given in
comma-separated LIST as follows:
+feature or feature enable feature
-feature disable feature
Known features and default setting:
warn-local ($ed[$default_feature{'warn-local'}]) warn if files in /usr/local are used
discard-check-version ($ed[$default_feature{'discard-check-version'}]) discard execve with only
--version argument; this works around some
configure scripts that check for binaries they
don\'t use
trace-local ($ed[$default_feature{'trace-local'}]) also try to identify file
accesses in /usr/local
catch-alternatives ($ed[$default_feature{'catch-alternatives'}]) catch access to alternatives
discard-sgml-catalogs ($ed[$default_feature{'discard-sgml-catalogs'}]) discard access to SGML
catalogs; some SGML tools read all the
registered catalogs at startup.
--no-conf, --noconf Don\'t read devscripts config files;
must be the first option given
-h, --help Display this help and exit
-v, --version Output version information and exit
Default settings modified by devscripts configuration files:
$modified_conf_msg
EOF
}
sub version () {
print <<'EOF';
This is $progname, from the Debian devscripts package, version ###VERSION###
Copyright Bill Allombert <ballombe@debian.org> 2001.
Modifications copyright 2002, 2003 Julian Gilbey <jdg@debian.org>
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
EOF
}
# Main program
# Features:
# This are heuristics used to speed up the process.
# Since they may be considered as "kludges" or worse "bugs"
# by some, they can be deactivated
# 0 disabled by default, 1 enabled by default.
%feature = (
"warn-local" => 1,
"discard-check-version" => 1,
"trace-local" => 0,
"catch-alternatives" => 1,
"discard-sgml-catalogs" => 1,
);
%default_feature = %feature;
# First process configuration file options, then check for command-line
# options. This is pretty much boilerplate.
if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
$modified_conf_msg = " (no configuration files read)";
shift;
} else {
my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
my %config_vars = ('DPKG_DEPCHECK_OPTIONS' => '',);
my %config_default = %config_vars;
my $shell_cmd;
# Set defaults
foreach my $var (keys %config_vars) {
$shell_cmd .= qq[$var="$config_vars{$var}";\n];
}
$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
# Read back values
foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
my $shell_out = `/bin/bash -c '$shell_cmd'`;
@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
foreach my $var (sort keys %config_vars) {
if ($config_vars{$var} ne $config_default{$var}) {
$modified_conf_msg .= " $var=$config_vars{$var}\n";
}
}
$modified_conf_msg ||= " (none)\n";
chomp $modified_conf_msg;
if ($config_vars{'DPKG_DEPCHECK_OPTIONS'} ne '') {
unshift @ARGV, split(' ', $config_vars{'DPKG_DEPCHECK_OPTIONS'});
}
}
# Default option:
$opts{"pkgs"} = 'all';
$opts{"allpkgs"} = 0;
Getopt::Long::Configure('bundling', 'require_order');
GetOptions(
"h|help" => sub { usage(); exit; },
"v|version" => sub { version(); exit; },
"a|all" => sub { $opts{"allpkgs"} = 1; },
"b|build-depends" => sub { $opts{"pkgs"} = 'build'; },
"d|ignore-dev-deps" => sub { $opts{"pkgs"} = 'dev'; },
"m|min-deps" => sub { $opts{"pkgs"} = 'min'; },
"C|C-locale" => \$opts{"C"},
"no-C-locale|noC-locale" => sub { $opts{"C"} = 0; },
"l|list-files" => \$opts{"l"},
"no-list-files|nolist-files" => sub { $opts{"l"} = 0; },
"o|output=s" => \$opts{"o"},
"O|strace-output=s" => \$opts{"strace-output"},
"I|strace-input=s" => \$opts{"strace-input"},
"f|features=s" => \&process_features,
"no-conf" => \$opts{"noconf"},
"noconf" => \$opts{"noconf"},
) or do { usage; exit 1; };
if ($opts{"noconf"}) {
die
"$progname: --no-conf is only acceptable as the first command-line option!\n";
}
if ($opts{"pkgs"} eq 'all') {
$opts{"allpkgs"} = 0;
} else {
# We don't initialise the packages database before doing this check,
# as that takes quite some time
unless (system('dpkg -L build-essential >/dev/null 2>&1') >> 8 == 0) {
die
"You must have the build-essential package installed or use the --all option\n";
}
}
@ARGV > 0
or $opts{"strace-input"}
or die
"You need to specify a command! Run $progname --help for more info\n";
# Run the command and trace it to see what's going on
my @usedfiles = getusedfiles(@ARGV);
if ($opts{"o"}) {
$opts{"o"} =~ s%^(\s)%./$1%;
open STDOUT, "> $opts{'o'}"
or warn
"Cannot open $opts{'o'} for writing: $!\nTrying to use stdout instead\n";
} else {
# Visual space
print "\n\n";
print '-' x 70, "\n";
}
# Get each file once only, and drop any we are not interested in.
# Also, expand all symlinks so we get full pathnames of the real file accessed.
@usedfiles = filterfiles(@usedfiles);
# Forget about the few files we are expecting to see but can ignore
@usedfiles = SetMinus(\@usedfiles, \@known_files);
# For a message at the end
my $number_files_used = scalar @usedfiles;
# Initialise the packages database unless --all is given
my $packagedeps;
# @used_ess_files will contain those files used which are in essential packages
my @used_ess_files;
# Exclude essential and build-essential packages?
if ($opts{"pkgs"} ne 'all') {
$packagedeps = Devscripts::PackageDeps->fromStatus();
my @essential = PackagesMatch('^Essential: yes$');
my @essential_packages
= $packagedeps->full_dependencies('build-essential', @essential);
my @essential_files = PackagesToFiles(@essential_packages);
@used_ess_files = SetInter(\@usedfiles, \@essential_files);
@usedfiles = SetMinus(\@usedfiles, \@used_ess_files);
}
# Now let's find out which packages are used...
my @ess_packages = FilesToPackages(@used_ess_files);
my @packages = FilesToPackages(@usedfiles);
my %dep_packages = (); # packages which are depended upon by others
# ... and remove their files from the filelist
if ($opts{"l"}) {
# Have to do it slowly :-(
if ($opts{"allpkgs"}) {
print
"Files used in each of the needed build-essential or essential packages:\n";
foreach my $pkg (sort @ess_packages) {
my @pkgfiles = PackagesToFiles($pkg);
print "Files used in (build-)essential package $pkg:\n ",
join("\n ", SetInter(\@used_ess_files, \@pkgfiles)), "\n";
}
print "\n";
}
print "Files used in each of the needed packages:\n";
foreach my $pkg (sort @packages) {
my @pkgfiles = PackagesToFiles($pkg);
print "Files used in package $pkg:\n ",
join("\n ", SetInter(\@usedfiles, \@pkgfiles)), "\n";
# We take care to note any files used which
# do not appear in any package
@usedfiles = SetMinus(\@usedfiles, \@pkgfiles);
}
print "\n";
} else {
# We take care to note any files used which
# do not appear in any package
my @pkgfiles = PackagesToFiles(@packages);
@usedfiles = SetMinus(\@usedfiles, \@pkgfiles);
}
if ($opts{"pkgs"} eq 'dev') {
# We also remove any direct dependencies of '-dev' packages
my %pkgs;
@pkgs{@packages} = (1) x @packages;
foreach my $pkg (@packages) {
next unless $pkg =~ /-dev$/;
my @deps = $packagedeps->dependencies($pkg);
foreach my $dep (@deps) {
$dep = $$dep[0] if ref $dep;
if (exists $pkgs{$dep}) {
$dep_packages{$dep} = $pkg;
delete $pkgs{$dep};
}
}
}
@packages = keys %pkgs;
} elsif ($opts{"pkgs"} eq 'min') {
# Do a mindep job on the package list
my ($packages_ref, $dep_packages_ref)
= $packagedeps->min_dependencies(@packages);
@packages = @$packages_ref;
%dep_packages = %$dep_packages_ref;
}
print "Summary: $number_files_used files considered.\n" if $opts{"l"};
# Ignore unrecognised /var/... files
@usedfiles = grep !/^\/var\//, @usedfiles;
if (@usedfiles) {
warn "The following files did not appear to belong to any package:\n";
warn join("\n", @usedfiles) . "\n";
}
print "Packages ", ($opts{"pkgs"} eq 'all') ? "used" : "needed", ":\n ";
print join("\n ", @packages), "\n";
if ($opts{"allpkgs"}) {
if (@ess_packages) {
print "\n(Build-)Essential packages used:\n ";
print join("\n ", @ess_packages), "\n";
} else {
print "\nNo (Build-)Essential packages used\n";
}
if (scalar keys %dep_packages) {
print "\nOther packages used with depending packages listed:\n";
foreach my $pkg (sort keys %dep_packages) {
print " $pkg <= $dep_packages{$pkg}\n";
}
}
}
exit 0;
### Subroutines
# This sub is handed two arguments: f or feature, and the setting
sub process_features ($$) {
foreach (split(',', $_[1])) {
my $state = 1;
m/^-/ and $state = 0;
s/^[-+]//;
if (exists $feature{$_}) {
$feature{$_} = $state;
} else {
die("Unknown feature $_\n");
}
}
}
# Get used files. This runs the requested command (given as parameters
# to this sub) under strace and then parses the output, returning a list
# of all absolute filenames successfully opened or execve'd.
sub getusedfiles (@) {
my $file;
if ($opts{"strace-input"}) {
$file = $opts{"strace-input"};
} else {
my $old_locale = $ENV{'LC_ALL'} || undef;
$file = $opts{"strace-output"}
|| `mktemp --tmpdir dpkg-depcheck.XXXXXXXXXX`;
chomp $file;
$file =~ s%^(\s)%./$1%;
my @strace_cmd = (
'strace', '-e', 'trace=open,openat,execve', '-f',
'-q', '-o', $file, @_
);
$ENV{'LC_ALL'} = "C" if $opts{"C"};
system(@strace_cmd);
$? >> 8 == 0
or die "Running strace failed (command line:\n@strace_cmd\n";
if (defined $old_locale) { $ENV{'LC_ALL'} = $old_locale; }
else { delete $ENV{'LC_ALL'}; }
}
my %openfiles = ();
open FILE, $file or die "Cannot open $file for reading: $!\n";
while (<FILE>) {
# We only consider absolute filenames
m/^\d+\s+(\w+)\((?:[\w\d_]*, )?\"(\/.*?)\",.*\) = (-?\d+)/ or next;
my ($syscall, $filename, $status) = ($1, $2, $3);
if ($syscall eq 'open' || $syscall eq 'openat') {
next unless $status >= 0;
} elsif ($syscall eq 'execve') {
next unless $status == 0;
} else {
next;
} # unrecognised syscall
next
if $feature{"discard-check-version"}
and m/execve\(\"\Q$filename\E\", \[\"[^\"]+\", "--version"\], /;
# So it's a real file
$openfiles{$filename} = 1;
}
unlink $file unless $opts{"strace-input"} or $opts{"strace-output"};
return keys %openfiles;
}
# Select those files which we are interested in, as determined by the
# user-specified options
sub filterfiles (@) {
my %files = ();
my %local_files = ();
my %alternatives = ();
my $pwd = cwd();
foreach my $file (@_) {
next unless -f $file;
$file = Cwd::abs_path($file);
my @links = ();
my $prevlink = '';
foreach (ListSymlinks($file, $pwd)) {
if (m%^/(usr|var)/local(/|\z)%) {
$feature{"warn-local"} and $local_files{$_} = 1;
unless ($feature{"trace-local"}) {
$prevlink = $_;
next;
}
} elsif ($feature{"discard-sgml-catalogs"}
and m%^/usr/share/(sgml/.*\.cat|.*/catalog)%) {
next;
} elsif ($feature{"catch-alternatives"} and m%^/etc/alternatives/%)
{
$alternatives{ "$prevlink --> " . readlink($_) } = 1
if $prevlink;
}
$prevlink = $_;
# If it's not in one of these dirs, we skip it
next unless m%^/(bin|etc|lib|sbin|usr|var)%;
push @links, $_;
}
@files{@links} = (1) x @links;
}
if (keys %local_files) {
print "warning: files in /usr/local or /var/local used:\n",
join("\n", sort keys %local_files), "\n";
}
if (keys %alternatives) {
print "warning: alternatives used:\n",
join("\n", sort keys %alternatives), "\n";
}
return keys %files;
}
# The purpose here is to find out all the symlinks crossed by a file access.
# We work from the end of the filename (basename) back towards the root of
# the filename (solving bug#246006 where /usr is a symlink to another
# filesystem), repeating this process until we end up with an absolute
# filename with no symlinks in it. We return a list of all of the
# full filenames encountered.
# For example, if /usr -> /moved/usr, then
# /usr/bin/X11/xapp would yield:
# /usr/bin/X11/xapp, /usr/X11R6/bin/xapp, /moved/usr/X11R6/bin/xapp
# input: file, pwd
# output: if symlink found: (readlink-replaced file, prefix)
# if not: (file, '')
sub NextSymlink ($) {
my $file = shift;
my $filestart = $file;
my $fileend = '';
while ($filestart ne '/') {
if (-l $filestart) {
my $link = readlink($filestart);
my $parent = dirname $filestart;
if ($link =~ m%^/%) { # absolute symlink
return $link . $fileend;
}
while ($link =~ s%^\./%%) { }
# The following is not actually correct: if we have
# /usr -> /moved/usr and /usr/mylib -> ../mylibdir, then
# /usr/mylib should resolve to /moved/mylibdir, not /mylibdir
# But if we try to take this into account, we would need to
# use something like Cwd(), which would immediately resolve
# /usr -> /moved/usr, losing us the opportunity of recognising
# the filename we want. This is a bug we'll probably have to
# cope with.
# One way of doing this correctly would be to have a function
# resolvelink which would recursively resolve any initial ../ in
# symlinks, but no more than that. But I don't really want to
# implement this unless it really proves to be necessary:
# people shouldn't be having evil symlinks like that on their
# system!!
while ($link =~ s%^\.\./%%) { $parent = dirname $parent; }
return $parent . '/' . $link . $fileend;
} else {
$fileend = '/' . basename($filestart) . $fileend;
$filestart = dirname($filestart);
}
}
return undef;
}
# input: file, pwd
# output: list of full filenames encountered en route
sub ListSymlinks ($$) {
my ($file, $path) = @_;
if ($file !~ m%^/%) { $file = "$path/$file"; }
my @fn = ($file);
while ($file = NextSymlink($file)) {
push @fn, $file;
}
return @fn;
}

View file

@ -0,0 +1,40 @@
.TH DPKG-GENBUILDDEPS 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
dpkg-genbuilddeps \- generate a list of packages used to build this package
.SH SYNOPSIS
\fBdpkg-genbuilddeps\fR [\fIarg\fR ...]
.SH DESCRIPTION
This program is a wrapper around \fBdpkg-depcheck\fR(1). It should be
run from the top of a Debian build tree. It calls
\fBdpkg-buildpackage\fR with any arguments given on the command line,
and by tracing the execution of this, it determines which
non-essential packages were used during the package building. This
can be useful in determining what the \fIBuild-Depends\fR control
fields should contain. It does not determine which packages were used
for the arch independent parts of the build and which for the arch
dependent parts, not does it attempt to determine which versions of
packages are required. It should be able to run under \fBfakeroot\fR
rather than being run as root, as \fBfakeroot dpkg-genbuilddeps\fR, or
\fBdpkg-genbuilddeps \-rfakeroot\fR.
.PP
This program requires the build-essential package to be installed. If
it is not, please use \fBdpkg-depcheck\fR directly, with a command
such as
.nf
dpkg-depcheck \-\-all dpkg-buildpackage \-us \-uc \-b \-rfakeroot ...
.fi
All this program itself does is essentially to run the command:
.nf
dpkg-depcheck \-b dpkg-buildpackage \-us \-uc \-b \-rfakeroot [arg ...]
.fi
.SH "SEE ALSO"
.BR dpkg-depcheck (1),
.BR fakeroot (1)
.B The Debian Policy Manual,
sections on Build-Depends etc.
.SH AUTHOR
The original \fBdpkg-genbuilddeps\fR was written by Ben Collins
<bcollins@debian.org>. The current version is a simple wrapper around
\fBdpkg-depcheck\fR written by Bill Allombert <ballombe@debian.org>.
This manual page was written by Julian Gilbey <jdg@debian.org>.

41
scripts/dpkg-genbuilddeps.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/bash
set -e
PROGNAME=${0##*/}
if [ $# -gt 0 ]; then
case $1 in
-h|--help)
cat <<EOF
Usage: $PROGNAME [options] [<arg> ...]
Build package and generate build dependencies.
All args are passed to dpkg-buildpackage.
Options:
-h, --help This help
-v, --version Report version and exit
EOF
exit 1
;;
-v|--version)
echo "$PROGNAME wrapper for dpkg-depcheck:"
dpkg-depcheck --version
exit 1
;;
esac
fi
if ! [ -x debian/rules ]; then
echo "$PROGNAME must be run in the source package directory" >&2
exit 1
fi
if ! dpkg -L build-essential > /dev/null 2>&1
then
echo "You must have the build-essential package installed to use $PROGNAME" >&2
echo "You can try running the dpkg-depcheck program directly as:" >&2
echo "dpkg-depcheck --all dpkg-buildpackage -us -uc -b $*" >&2
exit 1
fi
echo "Warning: if this program hangs, kill it and read the manpage!" >&2
dpkg-depcheck -b dpkg-buildpackage -us -uc -b "$@"

33
scripts/dscextract.1 Normal file
View file

@ -0,0 +1,33 @@
.TH DSCEXTRACT 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
dscextract \- extract a single file from a Debian source package
.SH SYNOPSIS
\fBdscextract\fR [\fIoptions\fR] \fIdscfile\fR \fIfile\fR
.SH DESCRIPTION
\fBdscextract\fR reads a single file from a Debian source package. The idea is
to only look into \fI.diff.gz\fR files (source format 1.0) or \fI.debian.tar.gz/bz2\fR
files (source format 3.0) where possible, hence avoiding to unpack large
tarballs. It is most useful for files in the \fIdebian/\fR subdirectory.
\fIfile\fP is relative to the first level directory contained in the package,
i.e. with the first component stripped.
.SH OPTIONS
.TP
.B \fB\-f
"Fast" mode. For source format 1.0, avoid to fall back scanning the \fI.orig.tar.gz\fR
file if \fIfile\fR was not found in the \fI.diff.gz\fR. (For 3.0 packages, it is
assumed that \fIdebian/*\fR are exactly the contents of \fIdebian.tar.gz/bz2\fR.)
.SH "EXIT STATUS"
.TP
0
\fIfile\fR was extracted.
.TP
1
\fIfile\fR was not found in the source package.
.TP
2
An error occurred, like \fIdscfile\fR was not found.
.SH EXAMPLE
dscextract dds_2.1.1+ddd105-2.dsc debian/watch || test $? = 1
.SH AUTHOR
\fBdscextract\fR was written by Christoph Berg <myon@debian.org>.

View file

@ -0,0 +1,34 @@
# /usr/share/bash-completion/completions/dscextract
# Bash command completion for dscextract(1).
# Documentation: bash(1), section “Programmable Completion”.
# Copyright © 2015, Nicholas Bamber <nicholas@periapt.co.uk>
_dscextract()
{
local cur prev words cword _options
_init_completion || return
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W '-f' -- "$cur" ) )
elif [[ "$prev" == -f ]]; then
declare -a _compreply=( $( compgen -o filenames -G '*.dsc' ) )
COMPREPLY=( $( compgen -W "${_compreply[*]}" -- "$cur" ) )
elif [[ "$prev" == *.dsc ]]; then
declare -a _compreply=( $( tar tvf ${prev/.dsc/.debian.tar.*} 2>/dev/null | sed 's! \+! !g' | cut -d' ' -f6 ) )
COMPREPLY=( $( compgen -W "${_compreply[*]}" -- "$cur" ) )
else
declare -a _compreply=( $( compgen -W '-f' -o filenames -G '*.dsc' ) )
COMPREPLY=( $( compgen -W "${_compreply[*]}" -- "$cur" ) )
fi
return 0
} && complete -F _dscextract dscextract
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

121
scripts/dscextract.sh Executable file
View file

@ -0,0 +1,121 @@
#!/bin/sh
# dscextract.sh - Extract a single file from a Debian source package
# Copyright (C) 2011 Christoph Berg <myon@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
set -eu
PROGNAME=${0##*/}
die() {
echo "$*" >&2
exit 2
}
setzip() {
case $1 in
*.gz) ZIP=--gzip ;;
*.xz) ZIP=--xz ;;
*.lzma) ZIP=--lzma ;;
*.bz2) ZIP=--bzip2 ;;
esac
}
FAST=""
while getopts "f" opt ; do
case $opt in
f) FAST=yes ;;
*) exit 2 ;;
esac
done
# shift away args
shift $(($OPTIND - 1))
[ $# = 2 ] || die "Usage: $PROGNAME <dsc> <file>"
DSC="$1"
test -e "$DSC" || die "$DSC not found"
FILE="$2"
DSCDIR=$(dirname "$DSC")
WORKDIR=$(mktemp -d --tmpdir dscextract.XXXXXX)
trap 'rm -rf "$WORKDIR"' EXIT
if DIFFGZ=$(grep -E '^ [0-9a-f]{32,64} [0-9]+ [^ ]+\.diff\.(gz|xz|lzma|bz2)$' "$DSC") ; then
DIFFGZ=$(echo "$DIFFGZ" | cut -d ' ' -f 4 | head -n 1)
test -e "$DSCDIR/$DIFFGZ" || die "$DSCDIR/$DIFFGZ: not found"
filterdiff -p1 -i "$FILE" -z "$DSCDIR/$DIFFGZ" > "$WORKDIR/patch"
if test -s "$WORKDIR/patch" ; then
# case 1: file found in .diff.gz
if ! grep -q '^@@ -0,0 ' "$WORKDIR/patch" ; then
# case 1a: patch requires original file
ORIGTGZ=$(grep -E '^ [0-9a-f]{32,64} [0-9]+ [^ ]+\.orig\.tar\.(gz|xz|lzma|bz2)$' "$DSC") || die "no orig.tar.* found in $DSC"
ORIGTGZ=$(echo "$ORIGTGZ" | cut -d ' ' -f 4 | head -n 1)
setzip $ORIGTGZ
test -e "$DSCDIR/$ORIGTGZ" || die "$DSCDIR/$ORIGTGZ not found"
tar --extract --to-stdout $ZIP --file "$DSCDIR/$ORIGTGZ" --wildcards "*/$FILE" > "$WORKDIR/output" 2>/dev/null || :
test -s "$WORKDIR/output" || die "$FILE not found in $DSCDIR/$ORIGTGZ, but required by patch"
fi
patch --silent "$WORKDIR/output" < "$WORKDIR/patch"
test -s "$WORKDIR/output" || die "patch $FILE did not produce any output"
cat "$WORKDIR/output"
exit 0
elif [ "$FAST" ] ; then
# in fast mode, don't bother looking into .orig.tar.gz
exit 1
fi
fi
if DEBIANTARGZ=$(grep -E '^ [0-9a-f]{32,64} [0-9]+ [^ ]+\.debian\.tar\.(gz|xz|lzma|bz2)$' "$DSC") ; then
case $FILE in
debian/*)
DEBIANTARGZ=$(echo "$DEBIANTARGZ" | cut -d ' ' -f 4 | head -n 1)
test -e "$DSCDIR/$DEBIANTARGZ" || die "$DSCDIR/$DEBIANTARGZ not found"
setzip $DEBIANTARGZ
tar --extract --to-stdout $ZIP --file "$DSCDIR/$DEBIANTARGZ" "$FILE" > "$WORKDIR/output" 2>/dev/null || :
test -s "$WORKDIR/output" || exit 1
# case 2a: file found in .debian.tar.gz
cat "$WORKDIR/output"
exit 0
# for 3.0 format, no need to look in other places here
;;
*)
ORIGTGZ=$(grep -E '^ [0-9a-f]{32,64} [0-9]+ [^ ]+\.orig\.tar\.(gz|xz|lzma|bz2)$' "$DSC") || die "no orig.tar.gz found in $DSC"
ORIGTGZ=$(echo "$ORIGTGZ" | cut -d ' ' -f 4 | head -n 1)
test -e "$DSCDIR/$ORIGTGZ" || die "$DSCDIR/$ORIGTGZ not found"
setzip $ORIGTGZ
tar --extract --to-stdout $ZIP --file "$DSCDIR/$ORIGTGZ" --wildcards --no-wildcards-match-slash "*/$FILE" > "$WORKDIR/output" 2>/dev/null || :
test -s "$WORKDIR/output" || exit 1
# case 2b: file found in .orig.tar.gz
# TODO: apply patches from debian.tar.gz
cat "$WORKDIR/output"
exit 0
;;
esac
fi
if TARGZ=$(grep -E '^ [0-9a-f]{32,64} [0-9]+ [^ ]+\.tar\.(gz|xz|lzma|bz2)$' "$DSC") ; then
TARGZ=$(echo "$TARGZ" | cut -d ' ' -f 4 | head -n 1)
test -e "$DSCDIR/$TARGZ" || die "$DSCDIR/$TARGZ not found"
setzip $TARGZ
tar --extract --to-stdout $ZIP --file "$DSCDIR/$TARGZ" --wildcards --no-wildcards-match-slash "*/$FILE" > "$WORKDIR/output" 2>/dev/null || :
test -s "$WORKDIR/output" || exit 1
# case 3: file found in .tar.gz or .orig.tar.gz
cat "$WORKDIR/output"
exit 0
fi
exit 1

88
scripts/dscverify.1 Normal file
View file

@ -0,0 +1,88 @@
.TH DSCVERIFY 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
dscverify \- verify the validity of a Debian package
.SH SYNOPSIS
\fBdscverify\fR [\fB\-\-keyring \fIkeyring\fR] ... \fIchanges_or_buildinfo_or_dsc_filename\fR ...
.SH DESCRIPTION
\fBdscverify\fR checks that the GPG signatures on the given
\fI.changes\fR, \fI.buildinfo\fP or \fI.dsc\fR files are good signatures
made by keys in the current Debian keyrings, found in the \fIdebian-keyring\fR
and \fIdebian-tag2upload-keyring\fR
packages. (Additional keyrings can be specified using the
\fB--keyring\fR option any number of times.) It then checks that the
other files listed in the \fI.changes\fR, \fI.buildinfo\fP or \fI.dsc\fR
files have the
correct sizes and checksums (MD5 plus SHA1 and SHA256 if the latter are
present). The exit status is 0 if there are no problems and non-zero
otherwise.
.SH OPTIONS
.TP
.BI \-\-keyring " " \fIkeyring\fR
Add \fIkeyring\fR to the list of keyrings to be used.
.TP
\fB\-\-no-default-keyrings\fR
Do not use the default set of keyrings.
.TP
\fB\-\-no-conf\fR, \fB\-\-noconf\fR
Do not read any configuration files. This can only be used as the
first option given on the command-line.
.TP
\fB\-\-nosigcheck\fR, \fB\-\-no\-sig\-check\fR, \fB-u\fR
Skip the signature verification step. That is, only verify the sizes and
checksums of the files listed in the \fI.changes\fR, \fI.buildinfo\fP or
\fI.dsc\fR files.
.TP
\fB\-\-verbose\fR
Do not suppress GPG output.
.TP
.TP
.BR \-\-help ", " \-h
Display a help message and exit successfully.
.TP
.B \-\-version
Display version and copyright information and exit successfully.
.SH "CONFIGURATION VARIABLES"
The two configuration files \fI/etc/devscripts.conf\fR and
\fI~/.devscripts\fR are sourced by a shell in that order to set
configuration variables. Environment variable settings are ignored
for this purpose. If the first command line option given is
\fB\-\-noconf\fR or \fB\-\-no-conf\fR, then these files will not be
read. The currently recognised variable is:
.TP
.B DSCVERIFY_KEYRINGS
This is a colon-separated list of extra keyrings to use in addition to
any specified on the command line.
.SH KEYRING
Please note that the keyring provided by the debian-keyring package
can be slightly out of date. The latest version can be obtained with
rsync, as documented in the README that comes with debian-keyring.
If you sync the keyring to a non-standard location (see below),
you can use the possibilities to specify extra keyrings, by either
using the above mentioned configuration option or the \-\-keyring option.
Below is an example for an alias:
alias dscverify='dscverify \-\-keyring ~/.gnupg/pubring.gpg'
.SH STANDARD KEYRING LOCATIONS
By default dscverify searches for the debian-keyring in the following
locations:
- ~/.gnupg/trustedkeys.gpg
- /srv/keyring.debian.org/keyrings/debian-keyring.gpg
- /usr/share/keyrings/debian-keyring.gpg
- /usr/share/keyrings/debian-tag2upload.pgp
- /usr/share/keyrings/debian-maintainers.gpg
- /usr/share/keyrings/debian-nonupload.gpg
.SH "SEE ALSO"
.BR gpg (1),
.BR devscripts.conf (5)
.SH AUTHOR
\fBdscverify\fR was written by Roderick Schertler <roderick@argon.org>
and posted on the debian-devel@lists.debian.org mailing list,
with several modifications by Julian Gilbey <jdg@debian.org>.

View file

@ -0,0 +1,32 @@
# /usr/share/bash-completion/completions/dscverify
# Bash command completion for dscverify(1).
# Documentation: bash(1), section “Programmable Completion”.
# Copyright © 2015, Nicholas Bamber <nicholas@periapt.co.uk>
_dscverify()
{
local cur prev words cword _options
_init_completion || return
if [[ "$cur" == -* ]]; then
_options='--keyring --no-default-keyrings --no-sig-check --verbose'
if [[ "$prev" == dscverify ]]; then
_options+=' --no-conf'
fi
COMPREPLY=( $( compgen -W "${_options}" -- "$cur" ) )
else
declare -a _compreply=( $( compgen -o filenames -G '*.@(dsc|changes)' ) )
COMPREPLY=( $( compgen -W "${_compreply[*]}" -- "$cur" ) )
fi
return 0
} && complete -F _dscverify dscverify
# Local variables:
# coding: utf-8
# mode: shell-script
# indent-tabs-mode: nil
# End:
# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 :

458
scripts/dscverify.pl Executable file
View file

@ -0,0 +1,458 @@
#!/usr/bin/perl
# This program takes .changes or .dsc files as arguments and verifies
# that they're properly signed by a Debian developer, and that the local
# copies of the files mentioned in them match the MD5 sums given.
# Copyright 1998 Roderick Schertler <roderick@argon.org>
# Modifications copyright 1999,2000,2002 Julian Gilbey <jdg@debian.org>
# Drastically simplified to match katie's signature checking Feb 2002
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use 5.004; # correct pipe close behavior
use strict;
use warnings;
use Cwd;
use Fcntl;
use Digest::MD5;
use Dpkg::IPC;
use Dpkg::Path qw(find_command);
use File::HomeDir;
use File::Spec;
use File::Temp;
use File::Basename;
use POSIX qw(:errno_h);
use Getopt::Long qw(:config bundling permute no_getopt_compat);
use List::Util qw(first);
my $progname = basename $0;
my $modified_conf_msg;
my $Exit = 0;
my $start_dir = cwd;
my $verify_sigs = 1;
my $use_default_keyrings = 1;
my $verbose = 0;
my $havegpg = first { find_command($_) } qw(gpg);
sub usage {
print <<"EOF";
Usage: $progname [options] changes-or-buildinfo-dsc-file ...
Options: --help Display this message
--version Display version and copyright information
--keyring <keyring>
Add <keyring> to the list of keyrings used
--no-default-keyrings
Do not check against the default keyrings
--nosigcheck, --no-sig-check, -u
Do not verify the GPG signature
--no-conf, --noconf
Do not read the devscripts config file
--verbose
Do not suppress GPG output.
Default settings modified by devscripts configuration files:
$modified_conf_msg
EOF
}
my $version = <<"EOF";
This is $progname, from the Debian devscripts package, version ###VERSION###
This code is copyright 1998 Roderick Schertler <roderick\@argon.org>
Modifications are copyright 1999, 2000, 2002 Julian Gilbey <jdg\@debian.org>
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
EOF
sub xwarndie_mess {
my @mess = ("$progname: ", @_);
$mess[$#mess] =~ s/:$/: $!\n/; # XXX loses if it's really /:\n/
return @mess;
}
sub xwarn {
warn xwarndie_mess @_;
$Exit ||= 1;
}
sub xdie {
die xwarndie_mess @_;
}
sub get_rings {
my @rings = @_;
my @keyrings = qw(/usr/share/keyrings/debian-keyring.gpg
/usr/share/keyrings/debian-maintainers.gpg
/usr/share/keyrings/debian-tag2upload.pgp
/usr/share/keyrings/debian-nonupload.gpg);
$ENV{HOME} = File::HomeDir->my_home;
if (defined $ENV{HOME} && -r "$ENV{HOME}/.gnupg/trustedkeys.gpg") {
unshift(@keyrings, "$ENV{HOME}/.gnupg/trustedkeys.gpg");
}
unshift(@keyrings, '/srv/keyring.debian.org/keyrings/debian-keyring.gpg');
if (system('dpkg-vendor', '--derives-from', 'Ubuntu') == 0) {
unshift(
@keyrings, qw(/usr/share/keyrings/ubuntu-master-keyring.gpg
/usr/share/keyrings/ubuntu-archive-keyring.gpg)
);
}
for (@keyrings) {
push @rings, $_ if -r;
}
return @rings if @rings;
xdie "can't find any system keyrings\n";
}
sub check_signature($\@;\$) {
my ($file, $rings, $outref) = @_;
my $fh = eval { File::Temp->new() }
or xdie "unable to open status file for gpg: $@\n";
# Allow the status file descriptor to pass on to the child process
my $flags = fcntl($fh, F_GETFD, 0);
fcntl($fh, F_SETFD, $flags & ~FD_CLOEXEC);
my $fd = fileno $fh;
my @cmd;
push @cmd, $havegpg, "--status-fd", $fd,
qw(--batch --no-options --no-default-keyring --always-trust);
foreach (@$rings) { push @cmd, '--keyring'; push @cmd, $_; }
push @cmd, '--verify', '--output', '-';
my ($out, $err) = ('', '');
eval {
spawn(
exec => \@cmd,
from_file => $file,
to_string => \$out,
error_to_string => \$err,
wait_child => 1
);
};
if ($@) {
print $out if ($verbose);
return $err || $@;
}
print $err if ($verbose);
seek($fh, 0, SEEK_SET);
my $status;
$status .= $_ while <$fh>;
close $fh;
if ($status !~ m/^\[GNUPG:\] VALIDSIG/m) {
return $out;
}
if (defined $outref) {
$$outref = $out;
}
return '';
}
sub process_file {
my ($file, @rings) = @_;
my ($filedir, $filebase);
my $sigcheck;
print "$file:\n";
# Move to the directory in which the file appears to live
chdir $start_dir or xdie "can't chdir to original directory!\n";
if ($file =~ m-(.*)/([^/]+)-) {
$filedir = $1;
$filebase = $2;
unless (chdir $filedir) {
xwarn "can't chdir $filedir:";
return;
}
} else {
$filebase = $file;
}
my $out;
if ($verify_sigs) {
$sigcheck = check_signature $filebase, @rings, $out;
if ($sigcheck) {
xwarn "$file failed signature check:\n$sigcheck";
return;
} else {
print " Good signature found\n";
}
} else {
if (!open SIGNED, '<', $filebase) {
xwarn "can't open $file:";
return;
}
$out = do { local $/; <SIGNED> };
if (!close SIGNED) {
xwarn "problem reading $file:";
return;
}
}
if ($file =~ /\.(changes|buildinfo)$/ and $out =~ /^Format:\s*(.*)$/mi) {
my $format = $1;
unless ($format =~ /^(\d+)\.(\d+)$/) {
xwarn "$file has an unrecognised format: $format\n";
return;
}
my ($major, $minor) = split /\./, $format;
$major += 0;
$minor += 0;
if (
$file =~ /\.changes$/ and ($major != 1 or $minor > 8)
or $file =~ /\.buildinfo$/ and (($major != 0 or $minor > 2)
and ($major != 1 or $minor > 0))
) {
xwarn "$file is an unsupported format: $format\n";
return;
}
}
my @spec = map { split /\n/ }
$out =~ /^(?:Checksums-Md5|Files):\s*\n((?:[ \t]+.*\n)+)/mgi;
unless (@spec) {
xwarn "no file spec lines in $file\n";
return;
}
my @checksums = map { split /\n/ } $out =~ /^Checksums-(\S+):\s*\n/mgi;
@checksums = grep { !/^(Md5|Sha(1|256))$/i } @checksums;
if (@checksums) {
xwarn "$file contains unsupported checksums:\n"
. join(", ", @checksums) . "\n";
return;
}
my %sha1s = map { reverse split /(\S+)\s*$/m }
$out =~ /^Checksums-Sha1:\s*\n((?:[ \t]+.*\n)+)/mgi;
my %sha256s = map { reverse split /(\S+)\s*$/m }
$out =~ /^Checksums-Sha256:\s*\n((?:[ \t]+.*\n)+)/mgi;
my $md5o = Digest::MD5->new or xdie "can't initialize MD5\n";
my $any;
for (@spec) {
unless (/^\s+([0-9a-f]{32})\s+(\d+)\s+(?:\S+\s+\S+\s+)?(\S+)\s*$/) {
xwarn "invalid file spec in $file `$_'\n";
next;
}
my ($md5, $size, $filename) = ($1, $2, $3);
my ($sha1, $sha1size, $sha256, $sha256size);
$filename !~ m,[/\x00],
or xdie "File name contains invalid characters: $file";
if (keys %sha1s) {
$sha1 = $sha1s{$filename};
unless (defined $sha1) {
xwarn "no sha1 for `$filename' in $file\n";
next;
}
unless ($sha1 =~ /^\s+([0-9a-f]{40})\s+(\d+)\s*$/) {
xwarn "invalid sha1 spec in $file `$sha1'\n";
next;
}
($sha1, $sha1size) = ($1, $2);
} else {
$sha1size = $size;
}
if (keys %sha256s) {
$sha256 = $sha256s{$filename};
unless (defined $sha256) {
xwarn "no sha256 for `$filename' in $file\n";
next;
}
unless ($sha256 =~ /^\s+([0-9a-f]{64})\s+(\d+)\s*$/) {
xwarn "invalid sha256 spec in $file `$sha256'\n";
next;
}
($sha256, $sha256size) = ($1, $2);
} else {
$sha256size = $size;
}
unless (open FILE, '<', $filename) {
if ($! == ENOENT) {
print STDERR " skipping $filename (not present)\n";
} else {
xwarn "can't read $filename:";
}
next;
}
$any = 1;
print " validating $filename\n";
# size
my $this_size = -s FILE;
unless (defined $this_size) {
xwarn "can't fstat $filename:";
next;
}
unless ($this_size == $size) {
xwarn
"invalid file length for $filename (wanted $size got $this_size)\n";
next;
}
unless ($this_size == $sha1size) {
xwarn
"invalid sha1 file length for $filename (wanted $sha1size got $this_size)\n";
next;
}
unless ($this_size == $sha256size) {
xwarn
"invalid sha256 file length for $filename (wanted $sha256size got $this_size)\n";
next;
}
# MD5
$md5o->reset;
$md5o->addfile(*FILE);
my $this_md5 = $md5o->hexdigest;
unless ($this_md5 eq $md5) {
xwarn "MD5 mismatch for $filename (wanted $md5 got $this_md5)\n";
next;
}
my $this_sha1;
eval {
spawn(
exec => ['sha1sum', $filename],
to_string => \$this_sha1,
wait_child => 1
);
};
($this_sha1) = split /\s/, $this_sha1, 2;
$this_sha1 ||= '';
unless (!keys %sha1s or $this_sha1 eq $sha1) {
xwarn
"SHA1 mismatch for $filename (wanted $sha1 got $this_sha1)\n";
next;
}
my $this_sha256;
eval {
spawn(
exec => ['sha256sum', $filename],
to_string => \$this_sha256,
wait_child => 1
);
};
($this_sha256) = split /\s/, $this_sha256, 2;
$this_sha256 ||= '';
unless (!keys %sha256s or $this_sha256 eq $sha256) {
xwarn
"SHA256 mismatch for $filename (wanted $sha256 got $this_sha256)\n";
next;
}
close FILE;
if ($filename =~ /\.(?:dsc|buildinfo)$/ && $verify_sigs) {
$sigcheck = check_signature $filename, @rings;
if ($sigcheck) {
xwarn "$filename failed signature check:\n$sigcheck";
next;
} else {
print " Good signature found\n";
}
}
}
$any
or xwarn "$file didn't specify any files present locally\n";
}
sub main {
@ARGV or xdie "no .changes, .buildinfo or .dsc files specified\n";
my @rings;
# Handle config file unless --no-conf or --noconf is specified
# The next stuff is boilerplate
if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
$modified_conf_msg = " (no configuration files read)";
shift @ARGV;
} else {
my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
my %config_vars = ('DSCVERIFY_KEYRINGS' => '',);
my %config_default = %config_vars;
my $shell_cmd;
# Set defaults
foreach my $var (keys %config_vars) {
$shell_cmd .= "$var='$config_vars{$var}';\n";
}
$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
# Read back values
foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
my $shell_out = `/bin/bash -c '$shell_cmd'`;
@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
foreach my $var (sort keys %config_vars) {
if ($config_vars{$var} ne $config_default{$var}) {
$modified_conf_msg .= " $var=$config_vars{$var}\n";
}
}
$modified_conf_msg ||= " (none)\n";
chomp $modified_conf_msg;
$config_vars{'DSCVERIFY_KEYRINGS'} =~ s/^\s*:\s*//;
$config_vars{'DSCVERIFY_KEYRINGS'} =~ s/\s*:\s*$//;
@rings = split /\s*:\s*/, $config_vars{'DSCVERIFY_KEYRINGS'};
}
GetOptions(
'help' => sub { usage; exit 0; },
'version' => sub { print $version; exit 0; },
'sigcheck|sig-check!' => \$verify_sigs,
'u' => sub { $verify_sigs = 0 },
'noconf|no-conf' => sub {
die
"--$_[0] is only acceptable as the first command-line option!\n";
},
'default-keyrings!' => \$use_default_keyrings,
'keyring=s@' => sub {
my $ring = $_[1];
if (-r $ring) { push @rings, $ring; }
else { die "Keyring $ring unreadable\n" }
},
'verbose' => \$verbose,
)
or do {
usage;
exit 1;
};
@ARGV or xdie "no .changes, .buildinfo or .dsc files specified\n";
@rings = get_rings @rings if $use_default_keyrings and $verify_sigs;
for my $file (@ARGV) {
process_file $file, @rings;
}
return 0;
}
$Exit = main || $Exit;
$Exit = 1 if $Exit and not $Exit % 256;
if ($Exit) { print STDERR "Validation FAILED!!\n"; }
else { print "All files validated successfully.\n"; }
exit $Exit;

258
scripts/edit-patch.sh Executable file
View file

@ -0,0 +1,258 @@
#!/bin/sh
#
# Copyright (C) 2009 Canonical
#
# Authors:
# Michael Vogt
# Daniel Holbach
# David Futcher
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
set -e
PROGNAME=${0##*/}
PATCHSYSTEM="unknown"
PATCHNAME="no-patch-name"
PREFIX="debian/patches"
PATCH_DESC=$(cat<<EOF
## Description: add some description\
\n## Origin/Author: add some origin or author\
\n## Bug: bug URL
EOF
)
fatal_error() {
echo "$@" >&2
exit 1
}
# check if the given binary is installed and give an error if not
# arg1: binary
# arg2: error message
require_installed() {
if ! which "$1" >/dev/null; then
fatal_error "$2"
fi
}
ensure_debian_dir() {
if [ ! -e debian/control ] || [ ! -e debian/rules ]; then
fatal_error "Can not find debian/rules or debian/control. Not in a debian dir?"
fi
}
detect_patchsystem() {
if [ -e debian/patches/series -o \
"$(cat debian/source/format 2> /dev/null)" = "3.0 (quilt)" ]; then
PATCHSYSTEM="quilt"
require_installed quilt "no quilt found, is 'quilt' installed?"
else
PATCHSYSTEM="none"
PREFIX="debian/applied-patches"
fi
}
# remove full path if given
normalize_patch_path() {
PATCHNAME=${PATCHNAME##*/}
echo "Normalizing patch path to $PATCHNAME"
}
# ensure (for new patches) that:
normalize_patch_extension() {
# check if we have a patch already
if [ -e $PREFIX/$PATCHNAME ]; then
echo "Patch $PATCHNAME exists, not normalizing"
return
fi
# normalize name for new patches
PATCHNAME=${PATCHNAME%.*}
PATCHNAME="${PATCHNAME}.patch"
echo "Normalizing patch name to $PATCHNAME"
}
edit_patch_quilt() {
export QUILT_PATCHES=debian/patches
if [ -e $QUILT_PATCHES ]; then
top_patch=$(quilt top)
echo "Top patch: $top_patch"
fi
if [ -e $PREFIX/$1 ]; then
# if it's an existing patch and we are at the end of the stack,
# go back at the beginning
if ! quilt unapplied; then
quilt pop -a
fi
quilt push $1
else
# if it's a new patch, make sure we are at the end of the stack
if quilt unapplied >/dev/null; then
quilt push -a
fi
quilt new $1
fi
# use a sub-shell
quilt shell
quilt refresh
if [ -n $top_patch ]; then
echo "Reverting quilt back to $top_patch"
quilt pop $top_patch
fi
vcs_add $PREFIX/$1 $PREFIX/series
}
edit_patch_none() {
# Dummy edit-patch function, just display a warning message
echo "No patchsystem could be found so the patch was applied inline and a copy \
stored in debian/patches-applied. Please remember to mention this in your changelog."
}
add_patch_quilt() {
# $1 is the original patchfile, $2 the normalized name
# FIXME: use quilt import instead?
cp $1 $PREFIX/$2
if ! grep -q $2 $PREFIX/series; then
echo "$2" >> $PREFIX/series
fi
vcs_add $PREFIX/$2 $PREFIX/series
}
add_patch_none() {
# $1 is the original patchfile, $2 the normalized name
cp $1 $PREFIX/$2
vcs_add $PREFIX/$2
}
vcs_add() {
if [ -d .bzr ]; then
bzr add $@
elif [ -d .git ];then
git add $@
else
echo "Remember to add $@ to a VCS if you use one"
fi
}
vcs_commit() {
# check if debcommit is happy
if ! debcommit --noact 2>/dev/null; then
return
fi
# commit (if the user confirms)
debcommit --confirm
}
add_changelog() {
S="$PREFIX/$1: [DESCRIBE CHANGES HERE]"
if head -n1 debian/changelog|grep UNRELEASED; then
dch --append "$S"
else
dch --increment "$S"
fi
# let the user edit it
dch --edit
}
add_patch_tagging() {
# check if we have a description already
if grep "## Description:" $PREFIX/$1; then
return
fi
# if not, add one
RANGE=1,1
# make sure we keep the first line (for dpatch)
if head -n1 $PREFIX/$1|grep -q '^#'; then
RANGE=2,2
fi
sed -i ${RANGE}i"$PATCH_DESC" $PREFIX/$1
}
detect_patch_location() {
# Checks whether the specified patch exists in debian/patches or on the filesystem
FILENAME=${PATCHNAME##*/}
if [ -f "$PREFIX/$FILENAME" ]; then
PATCHTYPE="debian"
elif [ -f "$PATCHNAME" ]; then
PATCHTYPE="file"
PATCHORIG="$PATCHNAME"
else
if [ "$PATCHSYSTEM" = "none" ]; then
fatal_error "No patchsystem detected, cannot create new patch (no quilt?)"
else
PATCHTYPE="new"
fi
fi
}
handle_file_patch() {
if [ "$PATCHTYPE" = "file" ]; then
[ -f "$PATCHORIG" ] || fatal_error "No patch detected"
if [ "$PATCHSYSTEM" = "none" ]; then
# If we're supplied a file and there is no patchsys we apply it directly
# and store it in debian/applied patches
[ -d $PREFIX ] || mkdir $PREFIX
patch -p0 < "$PATCHORIG"
cp "$PATCHORIG" "$PREFIX/$PATCHNAME"
else
# Patch type is file but there is a patchsys present, so we add it
# correctly
cp "$PATCHORIG" "$PREFIX/$PATCHNAME"
if [ "$PATCHSYSTEM" = "quilt" ]; then
echo "$PATCHNAME" >> $PREFIX/series
fi
echo "Copying and applying new patch. You can now edit the patch or exit the subshell to save."
fi
fi
}
# TODO:
# - edit-patch --remove implementieren
main() {
# parse args
if [ $# -ne 1 ]; then
fatal_error "Need exactly one patch name"
fi
PATCHNAME="$1"
# do the work
ensure_debian_dir
detect_patchsystem
detect_patch_location
normalize_patch_path
normalize_patch_extension
handle_file_patch
if [ "${PROGNAME%.sh}" = edit-patch ]; then
edit_patch_$PATCHSYSTEM $PATCHNAME
elif [ "${PROGNAME%.sh}" = add-patch ]; then
add_patch_$PATCHSYSTEM $1 $PATCHNAME
else
fatal_error "Unknown script name: $0"
fi
add_patch_tagging $PATCHNAME
add_changelog $PATCHNAME
vcs_commit
}
main $@

42
scripts/getbuildlog.1 Normal file
View file

@ -0,0 +1,42 @@
.TH GETBUILDLOG 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
.SH NAME
getbuildlog \- download build logs from Debian auto\-builders
.SH SYNOPSIS
\fBgetbuildlog\fR \fIpackage\fR
[\fIversion\-pattern\fR]
[\fIarchitecture\-pattern\fR]
.SH DESCRIPTION
\fBgetbuildlog\fR downloads build logs of \fIpackage\fR from Debian
auto\-builders. It downloads build logs of all versions and for all
architectures if \fIversion\-pattern\fR and \fIarchitecture\-pattern\fR are
not specified or empty, otherwise only build logs whose versions match
\fIversion-pattern\fR and build logs whose architectures match
\fIarchitecture-pattern\fR will be downloaded. The version and architecture
patterns are interpreted as extended regular expressions as described in
\fBgrep\fR(1).
.PP
If \fIversion-pattern\fR is "last" then only the logs for the most
recent version of \fIpackage\fR found on buildd.debian.org will be
downloaded.
.PP
If \fIversion-pattern\fR is "last-all" then the logs for the most recent
version found on each build log index will be downloaded.
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Show usage information and examples.
.TP
\fB\-V\fR, \fB\-\-version\fR
Show version and copyright information.
.SH EXAMPLES
.TP
getbuildlog hello 2\\.2\-1 amd64
Download amd64 build log for hello version 2.2\-1.
.TP
getbuildlog glibc "" mips.*
Download mips(el) build logs of all glibc versions.
.TP
getbuildlog wesnoth .*bpo.*
Download all build logs of backported wesnoth versions.
.SH AUTHOR
Written by Frank S. Thomas <fst@debian.org>.

Some files were not shown because too many files have changed in this diff Show more