diff options
Diffstat (limited to 'scripts')
158 files changed, 47883 insertions, 0 deletions
diff --git a/scripts/Makefile b/scripts/Makefile new file mode 100644 index 0000000..0e3f4ad --- /dev/null +++ b/scripts/Makefile @@ -0,0 +1,168 @@ + +include ../Makefile.common +include /usr/share/dpkg/vendor.mk +DESTDIR = + +define \n + + +endef + +VERSION_FILE = ../version +VERSION = $(shell cat $(VERSION_FILE)) + +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 \ + 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 \ + 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 $<; \ + +test_py: $(VERSION_FILE) + $(foreach python,$(shell py3versions -r ../debian/control),$(python) setup.py test$(\n)) + +debbisect.1: debbisect + help2man \ + --name="bisect snapshot.debian.org" \ + --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) + + +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 diff --git a/scripts/annotate-output.1 b/scripts/annotate-output.1 new file mode 100644 index 0000000..2352221 --- /dev/null +++ b/scripts/annotate-output.1 @@ -0,0 +1,60 @@ +.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 will execute the specified program, while +prepending every line with the current time and O for stdout and E for +stderr. + +.SH OPTIONS +.TP +\fB+FORMAT\fR +Controls the timestamp format, as per \fBdate\fR(1). Defaults to +"%H:%M:%S". +.TP +\fB\-h\fR, \fB\-\-help\fR +Display a help message and exit successfully. + +.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 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). + +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 the executed program could +however cause this behaviour to show up less frequently. + +The program does not work as well when the output is not linewise. In +particular, when an interactive program asks for input, the question +might not be shown until after you have answered it. This will give +the impression that the annotated program has hung, while it has not. + +.SH "SEE ALSO" +\fBdate\fR(1) + +.SH SUPPORT +This program 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. diff --git a/scripts/annotate-output.sh b/scripts/annotate-output.sh new file mode 100755 index 0000000..84025f5 --- /dev/null +++ b/scripts/annotate-output.sh @@ -0,0 +1,92 @@ +#!/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 + +PROGNAME=${0##*/} + +handler() { + while IFS= read -r line; do + printf "%s %s: %s\n" "$($1)" "$2" "$line" + done + if [ -n "$line" ]; then + printf "%s %s: %s" "$($1)" "$2" "$line" + fi +} + +usage() { + echo \ +"Usage: $PROGNAME [options] program [args ...] + Run program and annotate STDOUT/STDERR with a timestamp. + + Options: + +FORMAT - Controls the timestamp format as per date(1) + -h, --help - Show this message" +} + +FMT="+%H:%M:%S" +while [ -n "${1-}" ]; do + case "$1" in + +*) + FMT="$1" + shift + ;; + -h|-help|--help) + usage + exit 0 + ;; + *) + break + ;; + esac +done + +if [ $# -lt 1 ]; then + usage + exit 1 +fi + +# shellcheck disable=SC2317 +plainfmt() { printf "%s" "$FMT"; } +# shellcheck disable=SC2317 +datefmt() { date "$FMT"; } +case "$FMT" in + *%*) formatter=datefmt;; + *) formatter=plainfmt; FMT="${FMT#+}";; +esac + +echo Started "$@" | handler $formatter 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 move 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). +err=0 +{ + { + { + { + { + "$@" 2>&1 1>&4 3>&- 4>&-; echo $? >&2; + } | handler $formatter E >&3; + } 4>&1 | handler $formatter O >&3; + } 2>&1; + } | { read -r xs; exit "$xs"; }; +} 3>&1 || err=$? + +echo "Finished with exitcode $err" | handler $formatter I +exit $err diff --git a/scripts/archpath.1 b/scripts/archpath.1 new file mode 100644 index 0000000..6425645 --- /dev/null +++ b/scripts/archpath.1 @@ -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. diff --git a/scripts/archpath.sh b/scripts/archpath.sh new file mode 100755 index 0000000..7fd943d --- /dev/null +++ b/scripts/archpath.sh @@ -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 diff --git a/scripts/bts.bash_completion b/scripts/bts.bash_completion new file mode 100644 index 0000000..c5f6288 --- /dev/null +++ b/scripts/bts.bash_completion @@ -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 : diff --git a/scripts/bts.pl b/scripts/bts.pl new file mode 100755 index 0000000..477fc54 --- /dev/null +++ b/scripts/bts.pl @@ -0,0 +1,4341 @@ +#!/usr/bin/perl + +# bts: This program provides a convenient interface to the Debian +# Bug Tracking System. +# +# Written by Joey Hess <joeyh@debian.org> +# Modifications by Julian Gilbey <jdg@debian.org> +# Modifications by Josh Triplett <josh@freedesktop.org> +# Copyright 2001-2003 Joey Hess <joeyh@debian.org> +# Modifications Copyright 2001-2003 Julian Gilbey <jdg@debian.org> +# Modifications Copyright 2007 Josh Triplett <josh@freedesktop.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 our own subclass of Pod::Text to +# a) Strip the POD markup before displaying it via "bts help" +# b) Automatically display the text which is supposed to be replaced by the +# user between <>, as per convention. +package Pod::BTS; +use strict; + +use base qw(Pod::Text); + +sub cmd_i { return '<' . $_[2] . '>' } + +package main; + +=head1 NAME + +bts - developers' command line interface to the Debian Bug Tracking System + +=cut + +use 5.010; # for defined-or +use strict; +use warnings; +use File::Basename; +use File::Copy; +use File::HomeDir; +use File::Path qw(make_path rmtree); +use File::Spec; +use File::Temp qw/tempfile/; +use Net::SMTP; +use Cwd; +use IO::File; +use IO::Handle; +use Devscripts::DB_File_Lock; +use Devscripts::Debbugs; +use Fcntl qw(O_RDWR O_RDONLY O_CREAT F_SETFD); +use Getopt::Long; +use Encode; +# Need support for ; as query param separator +use URI 1.37; +use URI::QueryParam; + +use Scalar::Util qw(looks_like_number); +use POSIX qw(locale_h strftime); + +setlocale(LC_TIME, "C"); # so that strftime is locale independent + +# Funny UTF-8 warning messages from HTML::Parse should be ignorable (#292671) +$SIG{'__WARN__'} = sub { + warn $_[0] + unless $_[0] + =~ /^Parsing of undecoded UTF-8 will give garbage when decoding entities/; +}; + +my $it = undef; +my $last_user = ''; +my $lwp_broken = undef; +my $authen_sasl_broken; +my $ua; + +sub have_lwp() { + return ($lwp_broken ? 0 : 1) if defined $lwp_broken; + eval { + require LWP; + require LWP::UserAgent; + require HTTP::Status; + require HTTP::Date; + }; + + if ($@) { + if ($@ =~ m%^Can\'t locate LWP%) { + $lwp_broken = "the libwww-perl package is not installed"; + } else { + $lwp_broken = "couldn't load LWP::UserAgent: $@"; + } + } else { + $lwp_broken = ''; + } + return $lwp_broken ? 0 : 1; +} + +sub have_authen_sasl() { + return ($authen_sasl_broken ? 0 : 1) if defined $authen_sasl_broken; + eval { require Authen::SASL; }; + + if ($@) { + if ($@ =~ m%^Can't locate Authen/SASL%) { + $authen_sasl_broken + = 'the libauthen-sasl-perl package is not installed'; + } else { + $authen_sasl_broken = "couldn't load Authen::SASL: $@"; + } + } else { + $authen_sasl_broken = ''; + } + return $authen_sasl_broken ? 0 : 1; +} + +# Constants +sub MIRROR_ERROR { 0; } +sub MIRROR_DOWNLOADED { 1; } +sub MIRROR_UP_TO_DATE { 2; } +my $NONPRINT = "\\x00-\\x1F\\x7F-\\xFF"; # we need this later for MIME stuff + +my $progname = basename($0); +my $modified_conf_msg; +my $debug = (exists $ENV{'DEBUG'} and $ENV{'DEBUG'}) ? 1 : 0; + +# Program version handling +# The BTS changed its format :/ Pages downloaded using old versions +# of bts won't look very good, so we force updating if the last cached +# version was downloaded by a devscripts version less than +# $new_cache_format_version +my $version = '###VERSION###'; +$version = '2.9.6' if $version =~ /\#/; # for testing unconfigured version +my $new_cache_format_version = '2.9.6'; + +# The official list is mirrored +# bugs-mirror.debian.org:/srv/bugs.debian.org/etc/config +# in the variable @gTags; we copy it verbatim here. +# +# Note that it is also in the POD documentation in the bts_tag +# function below, look for "potato". +our (@gTags, @valid_tags, %valid_tags); +#<<< This variable definition should be kept verbatim from the BTS config +@gTags = ( "patch", "wontfix", "moreinfo", "unreproducible", + "help", "security", "upstream", "pending", "confirmed", + "ipv6", "lfs", "d-i", "l10n", "newcomer", "a11y", "ftbfs", + "fixed-upstream", "fixed", "fixed-in-experimental", + "sid", "experimental", + "potato", "woody", + "sarge", "sarge-ignore", "etch", "etch-ignore", + "lenny", "lenny-ignore", "squeeze", "squeeze-ignore", + "wheezy", "wheezy-ignore", "jessie", "jessie-ignore", + "stretch", "stretch-ignore", "buster", "buster-ignore", + "bullseye", "bullseye-ignore","bookworm","bookworm-ignore", + "trixie","trixie-ignore", + ); +#>>> + +*valid_tags = \@gTags; +%valid_tags = map { $_ => 1 } @valid_tags; +my @valid_severities = qw(wishlist minor normal important + serious grave critical); + +my $browser; # Will set if necessary + +$ENV{HOME} = File::HomeDir->my_home; +my $cachedir + = $ENV{XDG_CACHE_HOME} || File::Spec->catdir($ENV{HOME}, '.cache'); +$cachedir = File::Spec->catdir($cachedir, 'devscripts', 'bts'); + +my $timestampdb = File::Spec->catfile($cachedir, 'bts_timestamps.db'); +my $prunestamp = File::Spec->catfile($cachedir, 'bts_prune.timestamp'); + +my %timestamp; + +END { + # This works even if we haven't tied it + untie %timestamp; +} + +my %clonedbugs = (); +my %ccpackages = (); +my %ccsubmitters = (); + +=head1 SYNOPSIS + +B<bts> [I<options>] I<command> [I<args>] [B<#>I<comment>] [B<.>|B<,> I<command> [I<args>] [B<#>I<comment>]] ... + +=head1 DESCRIPTION + +This is a command line interface to the Debian Bug Tracking System +(BTS), intended mainly +for use by developers. It lets the BTS be manipulated using simple commands +that can be run at the prompt or in a script, does various sanity checks on +the input, and constructs and sends a mail to the BTS control address for +you. A local cache of web pages and e-mails from the BTS may also be +created and updated. + +In general, the command line interface is the same as what you would write +in a mail to control@bugs.debian.org, just prefixed with "bts". For +example: + + % bts severity 69042 normal + % bts merge 69042 43233 + % bts retitle 69042 blah blah + +A few additional commands have been added for your convenience, and this +program is less strict about what constitutes a valid bug number. For example, +"severity Bug#85942 normal" is understood, as is "severity #85942 normal". +(Of course, your shell may regard "#" as a comment character though, so you +may need to quote it!) + +Also, for your convenience, this program allows you to abbreviate commands +to the shortest unique substring (similar to how cvs lets you abbreviate +commands). So it understands things like "bts cl 85942". + +It is also possible to include a comment in the mail sent to the BTS. If +your shell does not strip out the comment in a command like +"bts severity 30321 normal #inflated severity", then this program is smart +enough to figure out where the comment is, and include it in the email. +Note that most shells do strip out such comments before they get to the +program, unless the comment is quoted. (Something like "bts +severity #85942 normal" will not be treated as a comment!) + +You can specify multiple commands by separating them with a single dot, +rather like B<update-rc.d>; a single comma may also be used; all the +commands will then be sent in a single mail. It is important the dot/comma is +surrounded by whitespace so it is not mistaken for part of a command. For +example (quoting where necessary so that B<bts> sees the comment): + + % bts severity 95672 normal , merge 95672 95673 \#they are the same! + +The abbreviation "it" may be used to refer to the last mentioned bug +number, so you could write: + + % bts severity 95672 wishlist , retitle it "bts: please add a --foo option" + +Please use this program responsibly, and do take our users into +consideration. + +=head1 OPTIONS + +B<bts> examines the B<devscripts> configuration files as described +below. Command line options override the configuration file settings, +though. + +=over 4 + +=item B<-o>, B<--offline> + +Make B<bts> use cached bugs for the B<show> and B<bugs> commands, if a cache +is available for the requested data. See the B<cache> command, below for +information on setting up a cache. + +=item B<--online>, B<--no-offline> + +Opposite of B<--offline>; overrides any configuration file directive to work +offline. + +=item B<-n>, B<--no-action> + +Do not send emails but print them to standard output. + +=item B<--cache>, B<--no-cache> + +Should we attempt to cache new versions of BTS pages when +performing B<show>/B<bugs> commands? Default is to cache. + +=item B<--cache-mode=>{B<min>|B<mbox>|B<full>} + +When running a B<bts cache> command, should we only mirror the basic +bug (B<min>), or should we also mirror the mbox version (B<mbox>), or should +we mirror the whole thing, including the mbox and the boring +attachments to the BTS bug pages and the acknowledgement emails (B<full>)? +Default is B<min>. + +=item B<--cache-delay=>I<seconds> + +Time in seconds to delay between each download, to avoid hammering the BTS +web server. Default is 5 seconds. + +=item B<--mbox> + +Open a mail reader to read the mbox corresponding to a given bug number +for B<show> and B<bugs> commands. + +=item B<--mailreader=>I<READER> + +Specify the command to read the mbox. Must contain a "B<%s>" string +(unquoted!), which will be replaced by the name of the mbox file. The +command will be split on white space and will not be passed to a +shell. Default is 'B<mutt -f %s>'. (Also, B<%%> will be substituted by a +single B<%> if this is needed.) + +=item B<--cc-addr=>I<CC_EMAIL_ADDRESS> + +Send carbon copies to a list of users. I<CC_EMAIL_ADDRESS> should be a +comma-separated list of email addresses. Multiple options add more CCs. + +=item B<--use-default-cc> + +Add the addresses specified in the configuration file option +B<BTS_DEFAULT_CC> to the list specified using B<--cc-addr>. This is the +default. + +=item B<--no-use-default-cc> + +Do not add addresses specified in B<BTS_DEFAULT_CC> to the carbon copy +list. + +=item B<--sendmail=>I<SENDMAILCMD> + +Specify the B<sendmail> command. The command will be split on white +space and will not be passed to a shell. Default is +F</usr/sbin/sendmail>. The B<-t> option will be automatically added if +the command is F</usr/sbin/sendmail> or F</usr/sbin/exim*>. For other +mailers, if they require a B<-t> option, this must be included in the +I<SENDMAILCMD>, for example: B<--sendmail="/usr/sbin/mymailer -t">. + +=item B<--mutt> + +Use B<mutt> for sending of mails. Default is not to use B<mutt>, except for some +commands. + +Note that one of B<$DEBEMAIL> or B<$EMAIL> must be set in the environment in order +to use B<mutt> to send emails. + +=item B<--no-mutt> + +Don't use B<mutt> for sending of mails. + +=item B<--soap-timeout=>I<SECONDS> + +Specify a timeout for SOAP calls as used by the B<select> and B<status> commands. + +=item B<--smtp-host=>I<SMTPHOST> + +Specify an SMTP host. If given, B<bts> will send mail by talking directly to +this SMTP host rather than by invoking a B<sendmail> command. + +The host name may be followed by a colon (":") and a port number in +order to use a port other than the default. It may also begin with +"ssmtp://" or "smtps://" to indicate that SMTPS should be used. + +If SMTPS not specified, B<bts> will still try to use STARTTLS if it's advertised +by the SMTP host. + +Note that one of B<$DEBEMAIL> or B<$EMAIL> must be set in the environment in order +to use direct SMTP connections to send emails. + +Note that when sending directly via an SMTP host, specifying addresses in +B<--cc-addr> or B<BTS_DEFAULT_CC> that the SMTP host will not relay will cause the +SMTP host to reject the entire mail. + +Note also that the use of the B<reassign> command may, when either B<--mutt> +or B<--force-interactive> mode is enabled, lead to the automatic addition of a Cc +to I<$newpackage>@packages.debian.org. In these cases, the note above regarding +relaying applies. The submission interface (port 587) on reportbug.debian.org +does not support relaying and, as such, should not be used as an SMTP server +for B<bts> under the circumstances described in this paragraph. + +=item B<--smtp-username=>I<USERNAME>, B<--smtp-password=>I<PASSWORD> + +Specify the credentials to use when connecting to the SMTP server +specified by B<--smtp-host>. If the server does not require authentication +then these options should not be used. + +If a username is specified but not a password, B<bts> will prompt for +the password before sending the mail. + +=item B<--smtp-helo=>I<HELO> + +Specify the name to use in the I<HELO> command when connecting to the SMTP +server; defaults to the contents of the file F</etc/mailname>, if it +exists. + +Note that some SMTP servers may reject the use of a I<HELO> which either +does not resolve or does not appear to belong to the host using it. + +=item B<--bts-server> + +Use a debbugs server other than https://bugs.debian.org. + +=item B<-f>, B<--force-refresh> + +Download a bug report again, even if it does not appear to have +changed since the last B<cache> command. Useful if a B<--cache-mode=full> is +requested for the first time (otherwise unchanged bug reports will not +be downloaded again, even if the boring bits have not been +downloaded). + +=item B<--no-force-refresh> + +Suppress any configuration file B<--force-refresh> option. + +=item B<--only-new> + +Download only new bugs when caching. Do not check for updates in +bugs we already have. + +=item B<--include-resolved> + +When caching bug reports, include those that are marked as resolved. This +is the default behaviour. + +=item B<--no-include-resolved> + +Reverse the behaviour of the previous option. That is, do not cache bugs +that are marked as resolved. + +=item B<--no-ack> + +Suppress acknowledgment mails from the BTS. Note that this will only +affect the copies of messages CCed to bugs, not those sent to the +control bot. + +=item B<--ack> + +Do not suppress acknowledgement mails. This is the default behaviour. + +=item B<-i>, B<--interactive> + +Before sending an e-mail to the control bot, display the content and +allow it to be edited, or the sending cancelled. + +=item B<--force-interactive> + +Similar to B<--interactive>, with the exception that an editor is spawned +before prompting for confirmation of the message to be sent. + +=item B<--no-interactive> + +Send control e-mails without confirmation. This is the default behaviour. + +=item B<-q>, B<--quiet> + +When running B<bts cache>, only display information about newly cached +pages, not messages saying already cached. If this option is +specified twice, only output error messages (to stderr). + +=item B<--no-conf>, B<--noconf> + +Do not read any configuration files. This can only be used as the +first option given on the command-line. + +=back + +=cut + +# Start by setting default values + +my $offlinemode = 0; +my $caching = 1; +my $cachemode = 'min'; +my $cachemode_re = '^(full|mbox|min)$'; +my $refreshmode = 0; +my $updatemode = 0; +my $mailreader = 'mutt -f %s'; +my $muttcmd = 'mutt -H %s'; +my $sendmailcmd = '/usr/sbin/sendmail'; +my $smtphost = ''; +my $smtpuser = ''; +my $smtppass = ''; +my $smtphelo = ''; +my $noaction = 0; + +# regexp for mailers which require a -t option +my $sendmail_t = '^/usr/sbin/sendmail$|^/usr/sbin/exim'; +my $includeresolved = 1; +my $requestack = 1; +my $interactive_re = '^(force|no|yes)$'; +my $interactive = 'no'; +my @ccemails = (); +my $toolname = ""; +my $btsserver = 'https://bugs.debian.org'; +my $use_mutt = 0; + +# Next, read 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 = ( + 'BTS_OFFLINE' => 'no', + 'BTS_CACHE' => 'yes', + 'BTS_CACHE_MODE' => 'min', + 'BTS_FORCE_REFRESH' => 'no', + 'BTS_ONLY_NEW' => 'no', + 'BTS_MAIL_READER' => 'mutt -f %s', + 'BTS_SENDMAIL_COMMAND' => '/usr/sbin/sendmail', + 'BTS_INCLUDE_RESOLVED' => 'yes', + 'BTS_SMTP_HOST' => '', + 'BTS_SMTP_AUTH_USERNAME' => '', + 'BTS_SMTP_AUTH_PASSWORD' => '', + 'BTS_SMTP_HELO' => '', + 'BTS_SUPPRESS_ACKS' => 'no', + 'BTS_INTERACTIVE' => 'no', + 'BTS_DEFAULT_CC' => '', + 'BTS_SERVER' => $btsserver, + ); + 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{'BTS_OFFLINE'} =~ /^(yes|no)$/ + or $config_vars{'BTS_OFFLINE'} = 'no'; + $config_vars{'BTS_CACHE'} =~ /^(yes|no)$/ + or $config_vars{'BTS_CACHE'} = 'yes'; + $config_vars{'BTS_CACHE_MODE'} =~ $cachemode_re + or $config_vars{'BTS_CACHE_MODE'} = 'min'; + $config_vars{'BTS_FORCE_REFRESH'} =~ /^(yes|no)$/ + or $config_vars{'BTS_FORCE_REFRESH'} = 'no'; + $config_vars{'BTS_ONLY_NEW'} =~ /^(yes|no)$/ + or $config_vars{'BTS_ONLY_NEW'} = 'no'; + $config_vars{'BTS_MAIL_READER'} =~ /\%s/ + or $config_vars{'BTS_MAIL_READER'} = 'mutt -f %s'; + $config_vars{'BTS_SENDMAIL_COMMAND'} =~ /./ + or $config_vars{'BTS_SENDMAIL_COMMAND'} = '/usr/sbin/sendmail'; + $config_vars{'BTS_INCLUDE_RESOLVED'} =~ /^(yes|no)$/ + or $config_vars{'BTS_INCLUDE_RESOLVED'} = 'yes'; + $config_vars{'BTS_SUPPRESS_ACKS'} =~ /^(yes|no)$/ + or $config_vars{'BTS_SUPPRESS_ACKS'} = 'no'; + $config_vars{'BTS_INTERACTIVE'} =~ $interactive_re + or $config_vars{'BTS_INTERACTIVE'} = 'no'; + + if (!length $config_vars{'BTS_SMTP_HOST'} + and $config_vars{'BTS_SENDMAIL_COMMAND'} ne '/usr/sbin/sendmail') { + my $cmd = (split ' ', $config_vars{'BTS_SENDMAIL_COMMAND'})[0]; + unless ($cmd =~ /^~?[A-Za-z0-9_\-\+\.\/]*$/) { + die +"BTS_SENDMAIL_COMMAND contained funny characters: $cmd\nPlease fix the configuration file.\n"; + } elsif (system("command -v $cmd >/dev/null 2>&1") != 0) { + die +"BTS_SENDMAIL_COMMAND $cmd could not be executed.\nPlease fix the configuration file.\n"; + } + } + + 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; + + $offlinemode = $config_vars{'BTS_OFFLINE'} eq 'yes' ? 1 : 0; + $caching = $config_vars{'BTS_CACHE'} eq 'no' ? 0 : 1; + $cachemode = $config_vars{'BTS_CACHE_MODE'}; + $refreshmode = $config_vars{'BTS_FORCE_REFRESH'} eq 'yes' ? 1 : 0; + $updatemode = $config_vars{'BTS_ONLY_NEW'} eq 'yes' ? 1 : 0; + $mailreader = $config_vars{'BTS_MAIL_READER'}; + $sendmailcmd = $config_vars{'BTS_SENDMAIL_COMMAND'}; + $smtphost = $config_vars{'BTS_SMTP_HOST'}; + $smtpuser = $config_vars{'BTS_SMTP_AUTH_USERNAME'}; + $smtppass = $config_vars{'BTS_SMTP_AUTH_PASSWORD'}; + $smtphelo = $config_vars{'BTS_SMTP_HELO'}; + $includeresolved = $config_vars{'BTS_INCLUDE_RESOLVED'} eq 'yes' ? 1 : 0; + $requestack = $config_vars{'BTS_SUPPRESS_ACKS'} eq 'no' ? 1 : 0; + $interactive = $config_vars{'BTS_INTERACTIVE'}; + push @ccemails, $config_vars{'BTS_DEFAULT_CC'} || (); + $btsserver = $config_vars{'BTS_SERVER'}; +} + +if (exists $ENV{'BUGSOFFLINE'}) { + warn +"BUGSOFFLINE environment variable deprecated: please use ~/.devscripts\nor --offline/-o option instead! (See bts(1) for details.)\n"; +} + +my ($opt_help, $opt_version, $opt_noconf); +my ($opt_cachemode, $opt_mailreader, $opt_sendmail, $opt_smtphost); +my ($opt_smtpuser, $opt_smtppass, $opt_smtphelo); +my $opt_cachedelay = 5; +my $opt_mutt; +my $opt_soap_timeout; +my $mboxmode = 0; +my $quiet = 0; +my @opt_ccemails = (); +my $use_default_cc = 1; +my $ccsecurity = ""; + +Getopt::Long::Configure(qw(gnu_compat bundling require_order)); +GetOptions( + "help|h" => \$opt_help, + "version" => \$opt_version, + "o" => \$offlinemode, + "offline!" => \$offlinemode, + "online" => sub { $offlinemode = 0; }, + "cache!" => \$caching, + "cache-mode|cachemode=s" => \$opt_cachemode, + "cache-delay=i" => \$opt_cachedelay, + "m|mbox" => \$mboxmode, + "mailreader|mail-reader=s" => \$opt_mailreader, + "cc-addr=s" => \@opt_ccemails, + "sendmail=s" => \$opt_sendmail, + "smtp-host|smtphost=s" => \$opt_smtphost, + "smtp-user|smtp-username=s" => \$opt_smtpuser, + "smtp-pass|smtp-password=s" => \$opt_smtppass, + "smtp-helo=s" => \$opt_smtphelo, + "f" => \$refreshmode, + "force-refresh!" => \$refreshmode, + "only-new!" => \$updatemode, + "n|no-action" => \$noaction, + "q|quiet+" => \$quiet, + "noconf|no-conf" => \$opt_noconf, + "include-resolved!" => \$includeresolved, + "ack!" => \$requestack, + "i|interactive" => sub { $interactive = 'yes'; }, + "no-interactive" => sub { $interactive = 'no'; }, + "force-interactive" => sub { $interactive = 'force'; }, + "use-default-cc!" => \$use_default_cc, + "toolname=s" => \$toolname, + "bts-server=s" => \$btsserver, + "mutt!" => \$opt_mutt, + "soap-timeout:i" => \$opt_soap_timeout, + ) + or die "Usage: $progname [options]\nRun $progname --help for more details\n"; + +if ($opt_noconf) { + die +"$progname: --no-conf is only acceptable as the first command-line option!\n"; +} +if ($opt_help) { bts_help(); exit 0; } +if ($opt_version) { bts_version(); exit 0; } + +if (!$use_default_cc) { + @ccemails = (); +} + +if (@opt_ccemails) { + push @ccemails, @opt_ccemails; +} + +if ($opt_mailreader) { + if ($opt_mailreader =~ /\%s/) { + $mailreader = $opt_mailreader; + } else { + warn +"$progname: ignoring invalid --mailreader option: invalid mail command following it.\n"; + } +} + +if ($opt_mutt) { + $use_mutt = 1; +} + +if ($opt_soap_timeout) { + Devscripts::Debbugs::soap_timeout($opt_soap_timeout); +} + +if ($opt_sendmail and $opt_smtphost) { + die "$progname: --sendmail and --smtp-host mutually exclusive\n"; +} elsif ($opt_mutt and $opt_sendmail) { + die "$progname: --sendmail and --mutt mutually exclusive\n"; +} elsif ($opt_mutt and $opt_smtphost) { + die "$progname: --smtp-host and --mutt mutually exclusive\n"; +} + +$smtphost = $opt_smtphost if $opt_smtphost; +$smtpuser = $opt_smtpuser if $opt_smtpuser; +$smtppass = $opt_smtppass if $opt_smtppass; +$smtphelo = $opt_smtphelo if $opt_smtphelo; + +if ($opt_sendmail) { + if ( $opt_sendmail ne '/usr/sbin/sendmail' + and $opt_sendmail ne $sendmailcmd) { + my $cmd = (split ' ', $opt_sendmail)[0]; + unless ($cmd =~ /^~?[A-Za-z0-9_\-\+\.\/]*$/) { + die "--sendmail command contained funny characters: $cmd\n"; + } elsif (system("command -v $cmd >/dev/null 2>&1") != 0) { + die "--sendmail command $cmd could not be executed.\n"; + } + } +} + +if ($opt_sendmail) { + $sendmailcmd = $opt_sendmail; + $smtphost = ''; +} else { + if (length $smtphost and !length $smtphelo) { + if (-e "/etc/mailname") { + if (open MAILNAME, '<', "/etc/mailname") { + $smtphelo = <MAILNAME>; + chomp $smtphelo; + close MAILNAME; + } else { + warn +"Unable to open /etc/mailname: $!\nUsing default HELO for SMTP\n"; + } + } + } +} + +if ($opt_cachemode) { + if ($opt_cachemode =~ $cachemode_re) { + $cachemode = $opt_cachemode; + } else { + warn +"$progname: ignoring invalid --cache-mode; must be one of min, mbox, full.\n"; + } +} + +if ($toolname) { + $toolname = " (using $toolname)"; +} + +my $btsurl; +if ($btsserver =~ m%^https?://(.*)/?$%) { + $btsurl = $btsserver . '/'; + $btsserver = $1; +} else { + $btsurl = "http://$btsserver/"; +} +$btsurl =~ s%//$%/%; +my $btscgiurl = $btsurl . 'cgi-bin/'; +if ($btsserver =~ /^debbugs\.gnu\.org/) { + $btscgiurl = $btsurl . 'cgi/'; +} +my $btscgipkgurl = $btscgiurl . 'pkgreport.cgi'; +my $btscgibugurl = $btscgiurl . 'bugreport.cgi'; +my $btscgispamurl = $btscgiurl . 'bugspam.cgi'; +my $btsemail = 'control@' . $btsserver; +my $packagesserver = ''; +if ($btsserver =~ /^bugs(-[\w-]+)?\.debian\.org/i) { + $packagesserver = 'packages.debian.org'; + $btscgispamurl =~ s|$btsurl|http://bugs-master.debian.org/|; +} +no warnings 'once'; +$Devscripts::Debbugs::btsurl = $btsurl; +use warnings 'once'; + +if (@ARGV == 0) { + bts_help(); + exit 0; +} + +# Otherwise, parse the arguments +my @command; +my @args; +our @comment = (''); +my $ncommand = 0; +my $iscommand = 1; +while (@ARGV) { + $_ = shift @ARGV; + if ($_ =~ /^[\.,]$/) { + next if $iscommand; # ". ." in command line - oops! + $ncommand++; + $iscommand = 1; + $comment[$ncommand] = ''; + } elsif ($iscommand) { + push @command, $_; + $iscommand = 0; + } elsif ($comment[$ncommand]) { + $comment[$ncommand] .= " $_"; + } elsif (/^\#/ and not /^\#\d+$/) { + $comment[$ncommand] = $_; + } else { + push @{ $args[$ncommand] }, $_; + } +} +$ncommand-- if $iscommand; + +# Grub through the symbol table to find matching commands. +my $subject = ''; +my $body = ''; +our $index; +for $index (0 .. $ncommand) { + no strict 'refs'; + if (exists $::{"bts_$command[$index]"}) { + "bts_$command[$index]"->(@{ $args[$index] }); + } elsif ($command[$index] =~ /^#/) { + mailbts('', $command[$index]); + } else { + my @matches = grep /^bts_\Q$command[$index]\E/, keys %::; + if (@matches != 1) { + die +"$progname: Couldn't find a unique match for the command $command[$index]!\nRun $progname --help for a list of valid commands.\n"; + } + + # Replace the abbreviated command with its expanded equivalent + $command[$index] = $matches[0]; + $command[$index] =~ s/^bts_//; + + $matches[0]->(@{ $args[$index] }); + } +} + +# Send all cached commands. +mailbtsall($subject, $body) if length $body; + +# Unnecessary, but we'll do this for clarity +exit 0; + +=head1 COMMANDS + +For full details about the commands, see the BTS documentation. +L<https://www.debian.org/Bugs/server-control> + +=over 4 + +=item B<show> [I<options>] [I<bug number> | I<package> | I<maintainer> | B<:> ] [I<opt>B<=>I<val> ...] + +=item B<show> [I<options>] [B<src:>I<package> | B<from:>I<submitter>] [I<opt>B<=>I<val> ...] + +=item B<show> [I<options>] [B<tag:>I<tag> | B<usertag:>I<tag> ] [I<opt>B<=>I<val> ...] + +=item B<show> [B<release-critical> | B<release-critical/>... | B<RC>] + +This is a synonym for B<bts bugs>. + +=cut + +sub bts_show { + goto &bts_bugs; +} + +=item B<bugs> [I<options>] [I<bug_number> | I<package> | I<maintainer> | B<:> ] [I<opt>B<=>I<val> ...] + +=item B<bugs> [I<options>] [B<src:>I<package> | B<from:>I<submitter>] [I<opt>B<=>I<val> ...] + +=item B<bugs> [I<options>] [B<tag:>I<tag> | B<usertag:>I<tag> ] [I<opt>B<=>I<val> ...] + +=item B<bugs> [B<release-critical> | B<release-critical/>... | B<RC>] + +Display the page listing the requested bugs in a web browser using +sensible-browser(1). + +Options may be specified after the B<bugs> command in addition to or +instead of options at the start of the command line: recognised +options at this point are: B<-o>/B<--offline>/B<--online>, B<-m>/B<--mbox>, B<--mailreader> +and B<-->[B<no->]B<cache>. These are described earlier in this manpage. If +either the B<-o> or B<--offline> option is used, or there is already an +up-to-date copy in the local cache, the cached version will be used. + +The meanings of the possible arguments are as follows: + +=over 8 + +=item (none) + +If nothing is specified, B<bts bugs> will display your bugs, assuming +that either B<DEBEMAIL> or B<EMAIL> (examined in that order) is set to the +appropriate email address. + +=item I<bug_number> + +Display bug number I<bug_number>. + +=item I<package> + +Display the bugs for the package I<package>. + +=item B<src:>I<package> + +Display the bugs for the source package I<package>. + +=item I<maintainer> + +Display the bugs for the maintainer email address I<maintainer>. + +=item B<from:>I<submitter> + +Display the bugs for the submitter email address I<submitter>. + +=item B<tag:>I<tag> + +Display the bugs which are tagged with I<tag>. + +=item B<usertag:>I<tag> + +Display the bugs which are tagged with usertag I<tag>. See the BTS +documentation for more information on usertags. This will require the +use of a B<users=>I<email> option. + +=item B<:> + +Details of the bug tracking system itself, along with a bug-request +page with more options than this script, can be found on +https://bugs.debian.org/. This page itself will be opened if the +command 'bts bugs :' is used. + +=item B<release-critical>, B<RC> + +Display the front page of the release-critical pages on the BTS. This +is a synonym for https://bugs.debian.org/release-critical/index.html. +It is also possible to say release-critical/debian/main.html and the like. +RC is a synonym for release-critical/other/all.html. + +=back + +After the argument specifying what to display, you can optionally +specify options to use to format the page or change what it displayed. +These are passed to the BTS in the URL downloaded. For example, pass +dist=stable to see bugs affecting the stable version of a package, +version=1.0 to see bugs affecting that version of a package, or reverse=yes +to display newest messages first in a bug log. + +If caching has been enabled (that is, B<--no-cache> has not been used, +and B<BTS_CACHE> has not been set to B<no>), then any page requested by +B<bts show> will automatically be cached, and be available offline +thereafter. Pages which are automatically cached in this way will be +deleted on subsequent "B<bts show>|B<bugs>|B<cache>" invocations if they have +not been accessed in 30 days. Warning: on a filesystem mounted with +the "noatime" option, running "B<bts show>|B<bugs>" does not update the cache +files' access times; a cached bug will then be subject to auto-cleaning +30 days after its initial download, even if it has been accessed in the +meantime. + +Any other B<bts> commands following this on the command line will be +executed after the browser has been exited. + +The desired browser can be specified and configured by setting the +B<BROWSER> environment variable. The conventions follow those defined by +Eric Raymond at http://www.catb.org/~esr/BROWSER/; we here reproduce the +relevant part. + +The value of B<BROWSER> may consist of a colon-separated series of +browser command parts. These should be tried in order until one +succeeds. Each command part may optionally contain the string B<%s>; if +it does, the URL to be viewed is substituted there. If a command part +does not contain B<%s>, the browser is to be launched as if the URL had +been supplied as its first argument. The string B<%%> must be substituted +as a single %. + +Rationale: We need to be able to specify multiple browser commands so +programs obeying this convention can do the right thing in either X or +console environments, trying X first. Specifying multiple commands may +also be useful for people who share files like F<.profile> across +multiple systems. We need B<%s> because some popular browsers have +remote-invocation syntax that requires it. Unless B<%%> reduces to %, it +won't be possible to have a literal B<%s> in the string. + +For example, on most Linux systems a good thing to do would be: + +BROWSER='mozilla -raise -remote "openURL(%s,new-window)":links' + +=cut + +sub bts_bugs { + @ARGV = @_; # needed for GetOptions + my ($sub_offlinemode, $sub_caching, $sub_mboxmode, $sub_mailreader); + GetOptions( + "o" => \$sub_offlinemode, + "offline!" => \$sub_offlinemode, + "online" => sub { $sub_offlinemode = 0; }, + "cache!" => \$sub_caching, + "m|mbox" => \$sub_mboxmode, + "mailreader|mail-reader=s" => \$sub_mailreader, + ) or die "$progname: unknown options for bugs command\n"; + @_ = @ARGV; # whatever's left + + if (defined $sub_offlinemode) { + ($offlinemode, $sub_offlinemode) = ($sub_offlinemode, $offlinemode); + } + if (defined $sub_caching) { + ($caching, $sub_caching) = ($sub_caching, $caching); + } + if (defined $sub_mboxmode) { + ($mboxmode, $sub_mboxmode) = ($sub_mboxmode, $mboxmode); + } + if (defined $sub_mailreader) { + if ($sub_mailreader =~ /\%s/) { + ($mailreader, $sub_mailreader) = ($sub_mailreader, $mailreader); + } else { + warn +"$progname: ignoring invalid --mailreader $sub_mailreader option:\ninvalid mail command following it.\n"; + $sub_mailreader = undef; + } + } + + my $url = sanitizething(shift); + if (!$url) { + if (defined $ENV{'DEBEMAIL'}) { + $url = $ENV{'DEBEMAIL'}; + } else { + if (defined $ENV{'EMAIL'}) { + $url = $ENV{'EMAIL'}; + } else { + die +"bts bugs: Please set DEBEMAIL or EMAIL to your Debian email address.\n"; + } + } + } + if ($url =~ /^.*\s<(.*)>\s*$/) { $url = $1; } + $url =~ s/^:$//; + + # Are there any options? + my $urlopts = ''; + if (@_) { + $urlopts = join(";", '', @_); # so it'll be ";opt1=val1;opt2=val2" + $urlopts =~ s/:/=/g; + $urlopts =~ s/;tag=/;include=/; + } + + browse($url, $urlopts); + + # revert options + if (defined $sub_offlinemode) { + $offlinemode = $sub_offlinemode; + } + if (defined $sub_caching) { + $caching = $sub_caching; + } + if (defined $sub_mboxmode) { + $mboxmode = $sub_mboxmode; + } + if (defined $sub_mailreader) { + $mailreader = $sub_mailreader; + } +} + +=item B<select> [I<key>B<:>I<value> ...] + +Uses the SOAP interface to output a list of bugs which match the given +selection requirements. + +The following keys are allowed, and may be given multiple times. + +=over 8 + +=item B<package> + +Binary package name. + +=item B<source> + +Source package name. + +=item B<maintainer> + +E-mail address of the maintainer. + +=item B<submitter> + +E-mail address of the submitter. + +=item B<severity> + +Bug severity. + +=item B<status> + +Status of the bug. One of B<open>, B<done>, or B<forwarded>. + +=item B<tag> + +Tags applied to the bug. If B<users> is specified, may include +usertags in addition to the standard tags. + +=item B<owner> + +Bug's owner. + +=item B<correspondent> + +Address of someone who sent mail to the log. + +=item B<affects> + +Bugs which affect this package. + +=item B<bugs> + +List of bugs to search within. + +=item B<users> + +Users to use when looking up usertags. + +=item B<archive> + +Whether to search archived bugs or normal bugs; defaults to B<0> +(i.e. only search normal bugs). As a special case, if archive is +B<both>, both archived and unarchived bugs are returned. + +=back + +For example, to select the set of bugs submitted by +jrandomdeveloper@example.com and tagged B<wontfix>, one would use + +bts select submitter:jrandomdeveloper@example.com tag:wontfix + +If a key is used multiple times then the set of bugs selected includes +those matching any of the supplied values; for example + +bts select package:foo severity:wishlist severity:minor + +returns all bugs of package foo with either wishlist or minor severity. + +=cut + +sub bts_select { + my @args = @_; + my $bugs = Devscripts::Debbugs::select(@args); + if (not defined $bugs) { + die "Error while retrieving bugs from SOAP server"; + } + print map { qq($_\n) } @{$bugs}; +} + +=item B<status> [I<bug> | B<file:>I<file> | B<fields:>I<field>[B<,>I<field> ...] | B<verbose>] ... + +Uses the SOAP interface to output status information for the given bugs +(or as read from the listed files -- use B<-> to indicate STDIN). + +By default, all populated fields for a bug are displayed. + +If B<verbose> is given, empty fields will also be displayed. + +If B<fields> is given, only those fields will be displayed. No validity +checking is performed on any specified fields. + +=cut + +sub bts_status { + my @args = @_; + + my @bugs; + my $showempty = 0; + my %field; + for my $bug (@args) { + if (looks_like_number($bug)) { + push @bugs, $bug; + } elsif ($bug =~ m{^file:(.+)}) { + my $file = $1; + my $fh; + if ($file eq '-') { + $fh = \*STDIN; + } else { + $fh = IO::File->new($file, 'r') + or die "Unable to open $file for reading: $!"; + } + while (<$fh>) { + chomp; + next if /^\s*\#/; + s/\s//g; + next unless looks_like_number($_); + push @bugs, $_; + } + } elsif ($bug =~ m{^fields:(.+)}) { + my $fields = $1; + for my $field (split /,/, $fields) { + $field{ lc $field } = 1; + } + $showempty = 1; + } elsif ($bug =~ m{^verbose$}) { + $showempty = 1; + } + } + my $bugs + = Devscripts::Debbugs::status(map { [bug => $_, indicatesource => 1] } + @bugs); + return if ($bugs eq ""); + + my $first = 1; + for my $bug (keys %{$bugs}) { + print "\n" if not $first; + $first = 0; + my @keys = grep { $_ ne 'bug_num' } + keys %{ $bugs->{$bug} }; + for my $key ('bug_num', @keys) { + if (%field) { + next unless exists $field{$key}; + } + my $out; + if (ref($bugs->{$bug}{$key}) eq 'ARRAY') { + $out .= join(',', @{ $bugs->{$bug}{$key} }); + } elsif (ref($bugs->{$bug}{$key}) eq 'HASH') { + $out .= join(',', + map { $_ . ' => ' . ($bugs->{$bug}{$key}{$_} || '') } + keys %{ $bugs->{$bug}{$key} }); + } else { + $out .= $bugs->{$bug}{$key} || ''; + } + if ($out || $showempty) { + print "$key\t$out\n"; + } + } + } +} + +=item B<clone> I<bug> I<new_ID> [I<new_ID> ...] + +The B<clone> control command allows you to duplicate a I<bug> report. It is useful +in the case where a single report actually indicates that multiple distinct +bugs have occurred. "New IDs" are negative numbers, separated by spaces, +which may be used in subsequent control commands to refer to the newly +duplicated bugs. A new report is generated for each new ID. + +=cut + +sub bts_clone { + my $bug = checkbug(shift) or die "bts clone: clone what bug?\n"; + @_ or die "bts clone: must specify at least one new ID\n"; + foreach (@_) { + $_ =~ /^-\d+$/ or die "bts clone: new IDs must be negative numbers\n"; + $clonedbugs{$_} = 1; + } + mailbts("cloning $bug", "clone $bug " . join(" ", @_)); +} + +sub common_close { + my $bug = checkbug(shift) or die "bts $command[$index]: close what bug?\n"; + my $version = shift; + $version = "" unless defined $version; + opts_done(@_); + mailbts("closing $bug", "close $bug $version"); + return $bug; +} + +# Do not include this in the manpage - it's deprecated +# +# =item B<close> I<bug> I<version> +# +# Close a I<bug>. Remember that using this to close a bug is often bad manners, +# sending an informative mail to nnnnn-done@bugs.debian.org is much better. +# You should specify which I<version> of the package closed the I<bug>, if +# possible. +# +# =cut + +sub bts_close { + my ($bug) = common_close(@_); + warn <<"EOT"; +$progname: Closing $bug as you requested. +Please note that the "$progname close" command is deprecated! +It is usually better to email nnnnnn-done\@$btsserver with +an informative mail. +Please remember to email $bug-submitter\@$btsserver with +an explanation of why you have closed this bug. Thank you! +EOT +} + +=item B<done> I<bug> [I<version>] + +Mark a I<bug> as Done. This forces interactive mode since done messages should +include an explanation why the bug is being closed. You should specify which +I<version> of the package closed the bug, if possible. + +=cut + +sub bts_done { + my ($bug) = common_close(@_); + # Force interactive mode since done mails shouldn't be sent without an + # explanation + if (not $use_mutt) { + $interactive = 'force'; + } + + # Include the submitter in the email, so we act like a mail to -done + $ccsubmitters{"$bug-submitter"} = 1; +} + +=item B<reopen> I<bug> [I<submitter>] + +Reopen a I<bug>, with optional I<submitter>. + +=cut + +sub bts_reopen { + my $bug = checkbug(shift) or die "bts reopen: reopen what bug?\n"; + my $submitter = shift || ''; # optional + opts_done(@_); + mailbts("reopening $bug", "reopen $bug $submitter"); +} + +=item B<archive> I<bug> + +Archive a I<bug> that has previously been archived but is currently not. +The I<bug> must fulfill all of the requirements for archiving with the +exception of those that are time-based. + +=cut + +sub bts_archive { + my $bug = checkbug(shift) or die "bts archive: archive what bug?\n"; + opts_done(@_); + mailbts("archiving $bug", "archive $bug"); +} + +=item B<unarchive> I<bug> + +Unarchive a I<bug> that is currently archived. + +=cut + +sub bts_unarchive { + my $bug = checkbug(shift) or die "bts unarchive: unarchive what bug?\n"; + opts_done(@_); + mailbts("unarchiving $bug", "unarchive $bug"); +} + +=item B<retitle> I<bug> I<title> + +Change the I<title> of the I<bug>. + +=cut + +sub bts_retitle { + my $bug = checkbug(shift) or die "bts retitle: retitle what bug?\n"; + my $title = join(" ", @_); + if (!length $title) { + die "bts retitle: set title of $bug to what?\n"; + } + mailbts("retitle $bug to $title", "retitle $bug $title"); +} + +=item B<summary> I<bug> [I<messagenum>] + +Select a message number that should be used as +the summary of a I<bug>. + +If no message number is given, the summary is cleared. + +=cut + +sub bts_summary { + my $bug = checkbug(shift) + or die "bts summary: change summary of what bug?\n"; + my $msg = shift || ''; + mailbts("summary $bug $msg", "summary $bug $msg"); +} + +=item B<submitter> I<bug> [I<bug> ...] I<submitter-email> + +Change the submitter address of a I<bug> or a number of bugs, with B<!> meaning +`use the address on the current email as the new submitter address'. + +=cut + +sub bts_submitter { + @_ or die "bts submitter: change submitter of what bug?\n"; + my $submitter = checkemail(pop, 1); + if (!defined $submitter) { + die "bts submitter: change submitter to what?\n"; + } + foreach (@_) { + my $bug = checkbug($_) + or die "bts submitter: $_ is not a bug number\n"; + mailbts("submitter $bug", "submitter $bug $submitter"); + } +} + +=item B<reassign> I<bug> [I<bug> ...] I<package> [I<version>] + +Reassign a I<bug> or a number of bugs to a different I<package>. +The I<version> field is optional; see the explanation at +L<https://www.debian.org/Bugs/server-control>. + +=cut + +sub bts_reassign { + my ($bug, @bugs); + while ($_ = shift) { + $bug = checkbug($_, 1) or last; + push @bugs, $bug; + } + @bugs or die "bts reassign: reassign what bug(s)?\n"; + my $package = $_ or die "bts reassign: reassign bug(s) to what package?\n"; + my $version = shift; + $version = "" unless defined $version; + if (length $version and $version !~ /\d/) { + die "bts reassign: version number $version contains no digits!\n"; + } + opts_done(@_); + + foreach $bug (@bugs) { + mailbts("reassign $bug to $package", + "reassign $bug $package $version"); + } + + foreach my $packagename (split /,/, $package) { + $packagename =~ s/^src://; + $ccpackages{$packagename} = 1; + } +} + +=item B<found> I<bug> [I<version>] + +Indicate that a I<bug> was found to exist in a particular package version. +Without I<version>, the list of fixed versions is cleared and the bug is +reopened. + +=cut + +sub bts_found { + my $bug = checkbug(shift) or die "bts found: found what bug?\n"; + my $version = shift; + if (!defined $version) { + warn +"$progname: found has no version number, but sending to the BTS anyway\n"; + $version = ""; + } + opts_done(@_); + mailbts("found $bug in $version", "found $bug $version"); +} + +=item B<notfound> I<bug> I<version> + +Remove the record that I<bug> was encountered in the given version of the +package to which it is assigned. + +=cut + +sub bts_notfound { + my $bug = checkbug(shift) or die "bts notfound: what bug?\n"; + my $version = shift + or die "bts notfound: remove record \#$bug from which version?\n"; + opts_done(@_); + mailbts("notfound $bug in $version", "notfound $bug $version"); +} + +=item B<fixed> I<bug> I<version> + +Indicate that a I<bug> was fixed in a particular package version, without +affecting the I<bug>'s open/closed status. + +=cut + +sub bts_fixed { + my $bug = checkbug(shift) or die "bts fixed: what bug?\n"; + my $version = shift or die "bts fixed: \#$bug fixed in which version?\n"; + opts_done(@_); + mailbts("fixed $bug in $version", "fixed $bug $version"); +} + +=item B<notfixed> I<bug> I<version> + +Remove the record that a I<bug> was fixed in the given version of the +package to which it is assigned. + +This is equivalent to the sequence of commands "B<found> I<bug> I<version>", +"B<notfound> I<bug> I<version>". + +=cut + +sub bts_notfixed { + my $bug = checkbug(shift) or die "bts notfixed: what bug?\n"; + my $version = shift + or die "bts notfixed: remove record \#$bug from which version?\n"; + opts_done(@_); + mailbts("notfixed $bug in $version", "notfixed $bug $version"); +} + +=item B<block> I<bug> B<by>|B<with> I<bug> [I<bug> ...] + +Note that a I<bug> is blocked from being fixed by a set of other bugs. + +=cut + +sub bts_block { + my $bug = checkbug(shift) or die "bts block: what bug is blocked?\n"; + my $word = shift; + if (defined $word && $word ne 'by' && $word ne 'with') { + unshift @_, $word; + } + @_ or die "bts block: need to specify at least two bug numbers\n"; + my @blockers; + foreach (@_) { + my $blocker = checkbug($_) + or die "bts block: some blocking bug number(s) not valid\n"; + push @blockers, $blocker; + } + mailbts("block $bug with @blockers", "block $bug with @blockers"); +} + +=item B<unblock> I<bug> B<by>|B<with> I<bug> [I<bug> ...] + +Note that a I<bug> is no longer blocked from being fixed by a set of other bugs. + +=cut + +sub bts_unblock { + my $bug = checkbug(shift) or die "bts unblock: what bug is blocked?\n"; + my $word = shift; + if (defined $word && $word ne 'by' && $word ne 'with') { + unshift @_, $word; + } + @_ or die "bts unblock: need to specify at least two bug numbers\n"; + my @blockers; + foreach (@_) { + my $blocker = checkbug($_) + or die "bts unblock: some blocking bug number(s) not valid\n"; + push @blockers, $blocker; + } + mailbts("unblock $bug with @blockers", "unblock $bug with @blockers"); +} + +=item B<merge> I<bug> I<bug> [I<bug> ...] + +Merge a set of bugs together. + +=cut + +sub bts_merge { + my @bugs; + foreach (@_) { + my $bug = checkbug($_) + or die "bts merge: some bug number(s) not valid\n"; + push @bugs, $bug; + } + @bugs > 1 + or die + "bts merge: at least two bug numbers to be merged must be specified\n"; + mailbts("merging @bugs", "merge @bugs"); +} + +=item B<forcemerge> I<bug> I<bug> [I<bug> ...] + +Forcibly merge a set of bugs together. The first I<bug> listed is the master bug, +and its settings (those which must be equal in a normal B<merge>) are assigned to +the bugs listed next. + +=cut + +sub bts_forcemerge { + my @bugs; + foreach (@_) { + my $bug = checkbug($_) + or die "bts forcemerge: some bug number(s) not valid\n"; + push @bugs, $bug; + } + @bugs > 1 + or die +"bts forcemerge: at least two bug numbers to be merged must be specified\n"; + mailbts("forcibly merging @bugs", "forcemerge @bugs"); +} + +=item B<unmerge> I<bug> + +Unmerge a I<bug>. + +=cut + +sub bts_unmerge { + my $bug = checkbug(shift) or die "bts unmerge: unmerge what bug?\n"; + opts_done(@_); + mailbts("unmerging $bug", "unmerge $bug"); +} + +=item B<tag> I<bug> [B<+>|B<->|B<=>] I<tag> [I<tag> ...] + +=item B<tags> I<bug> [B<+>|B<->|B<=>] I<tag> [I<tag> ...] + +Set or unset a I<tag> on a I<bug>. The tag may either be the exact tag name +or it may be abbreviated to any unique tag substring. (So using +B<fixed> will set the tag B<fixed>, not B<fixed-upstream>, for example, +but B<fix> would not be acceptable.) Multiple tags may be specified as +well. The two commands (tag and tags) are identical. At least one tag +must be specified, unless the B<=> flag is used, where the command + + bts tags <bug> = + +will remove all tags from the specified I<bug>. + +Adding/removing the B<security> tag will add "team\@security.debian.org" +to the Cc list of the control email. + +The list of valid tags and their significance is available at +L<https://www.debian.org/Bugs/Developer#tags>. The current valid tags +are: + +patch, wontfix, moreinfo, unreproducible, fixed, help, security, upstream, +pending, d-i, confirmed, ipv6, lfs, fixed-upstream, l10n, newcomer, +a11y, ftbfs + +There is also a tag for each release of Debian since "potato". Note +that this list may be out of date, see the website for the most up to +date source. + +=cut + +# note that the tag list is also in the @gtag variable, look for +# "potato" above. +sub bts_tags { + my $bug = checkbug(shift) or die "bts tags: tag what bug?\n"; + if (!@_) { + die "bts tags: set what tag?\n"; + } + # Parse the rest of the command line. + my $base_command = "tags $bug"; + my $commands = []; + + my $curop; + foreach my $tag (@_) { + if ($tag =~ s/^([-+=])//) { + my $op = $1; + if ($op eq '=') { + $curop = '='; + $commands = []; + $ccsecurity = ''; + } elsif (!$curop || $curop ne $op) { + $curop = $op; + } + next unless $tag; + } + if (!$curop) { + $curop = '+'; + } + if ($tag eq 'gift') { + my $gift_flag = $curop; + if ($gift_flag eq '=') { + $gift_flag = '+'; + } + # Backward compatibility: do both gift usertagging and newcomer + # tagging. Gifting should be removed after a suitable migration + # time. See https://wiki.debian.org/qa.debian.org/GiftTag header + # for more info. + mailbts("tagging $bug", "tags $bug + newcomer"); + mailbts( + "gifting $bug", +"user debian-qa\@lists.debian.org\nusertag $bug $gift_flag gift" + ); + next; + } + if (!exists $valid_tags{$tag}) { + # Try prefixes + my @matches = grep /^\Q$tag\E/, @valid_tags; + if (@matches != 1) { + die "bts tags: \"$tag\" is not a " + . (@matches > 1 ? "unique" : "valid") + . " tag prefix. Choose from: " + . join(" ", @valid_tags) . "\n"; + } + $tag = $matches[0]; + } + if (!@$commands || $curop ne $commands->[-1]{op}) { + push(@$commands, { op => $curop, tags => [] }); + } + push(@{ $commands->[-1]{tags} }, $tag); + if ($tag eq "security") { + $ccsecurity = "team\@security.debian.org"; + } + } + + my $command = ''; + foreach my $cmd (@$commands) { + if ($cmd->{op} ne '=' && !@{ $cmd->{tags} }) { + die "bts tags: set what tag?\n"; + } + $command .= " $cmd->{op} " . join(' ', @{ $cmd->{tags} }); + } + if (!$command && $curop eq '=') { + $command = " $curop"; + } + + if ($command) { + mailbts("tagging $bug", $base_command . $command); + } +} + +=item B<affects> I<bug> [B<+>|B<->|B<=>] I<package> [I<package> ...] + +Indicates that a I<bug> affects a I<package> other than that against which it is filed, causing +the I<bug> to be listed by default in the I<package> list of the other I<package>. This should +generally be used where the I<bug> is severe enough to cause multiple reports from users to be +assigned to the wrong package. At least one I<package> must be specified, unless +the B<=> flag is used, where the command + + bts affects <bug> = + +will remove all indications that I<bug> affects other packages. + +=cut + +sub bts_affects { + my $bug = checkbug(shift) + or die "bts affects: mark what bug as affecting another package?\n"; + + if (!@_) { + die "bts affects: mark which package as affected?\n"; + } + # Parse the rest of the command line. + my $command = "affects $bug"; + my $flag = ""; + if ($_[0] =~ /^[-+=]$/) { + $flag = $_[0]; + $command .= " $flag"; + shift; + } elsif ($_[0] =~ s/^([-+=])//) { + $flag = $1; + $command .= " $flag"; + } + + if ($flag ne '=' && !@_) { + die "bts affects: mark which package as affected?\n"; + } + + foreach my $package (@_) { + $command .= " $package"; + } + + mailbts("affects $bug", $command); +} + +=item B<user> I<email> + +Specify a user I<email> address before using the B<usertags> command. + +=cut + +sub bts_user { + my $email = checkemail(shift) + or die "bts user: set user to what email address?\n"; + if (!length $email) { + die "bts user: set user to what email address?\n"; + } + opts_done(@_); + if ($email ne $last_user) { + mailbts("user $email", "user $email"); + } + $last_user = $email; +} + +=item B<usertag> I<bug> [B<+>|B<->|B<=>] I<tag> [I<tag> ...] + +=item B<usertags> I<bug> [B<+>|B<->|B<=>] I<tag> [I<tag> ...] + +Set or unset a user tag on a I<bug>. The I<tag> must be the exact tag name wanted; +there are no defaults or checking of tag names. Multiple tags may be +specified as well. The two commands (B<usertag> and B<usertags>) are identical. +At least one I<tag> must be specified, unless the B<=> flag is used, where the +command + + bts usertags <bug> = + +will remove all user tags from the specified I<bug>. + +=cut + +sub bts_usertags { + my $bug = checkbug(shift) or die "bts usertags: tag what bug?\n"; + if (!@_) { + die "bts usertags: set what user tag?\n"; + } + # Parse the rest of the command line. + my $command = "usertags $bug"; + my $flag = ""; + if ($_[0] =~ /^[-+=]$/) { + $flag = $_[0]; + $command .= " $flag"; + shift; + } elsif ($_[0] =~ s/^([-+=])//) { + $flag = $1; + $command .= " $flag"; + } + + if ($flag ne '=' && !@_) { + die "bts usertags: set what user tag?\n"; + } + + $command .= sprintf(' %s', join(' ', @_)); + + mailbts("usertagging $bug", $command); +} + +=item B<claim> I<bug> [I<claim>] + +Record that you have claimed a I<bug> (e.g. for a bug squashing party). +I<claim> should be a unique token allowing the bugs you have claimed +to be identified; an e-mail address is often used. + +If no I<claim> is specified, the environment variable B<DEBEMAIL> +or B<EMAIL> (checked in that order) is used. + +=cut + +sub bts_claim { + my $bug = checkbug(shift) or die "bts claim: claim what bug?\n"; + my $claim = checkemail(shift) || $ENV{'DEBEMAIL'} || $ENV{'EMAIL'} || ""; + if (!length $claim) { + die "bts claim: use what claim token?\n"; + } + $claim = extractemail($claim); + bts_user("bugsquash\@qa.debian.org"); + bts_usertags("$bug", "+$claim"); +} + +=item B<unclaim> I<bug> [I<claim>] + +Remove the record that you have claimed a bug. + +If no I<claim> is specified, the environment variable B<DEBEMAIL> +or B<EMAIL> (checked in that order) is used. + +=cut + +sub bts_unclaim { + my $bug = checkbug(shift) or die "bts unclaim: unclaim what bug?\n"; + my $claim = checkemail(shift) || $ENV{'DEBEMAIL'} || $ENV{'EMAIL'} || ""; + if (!length $claim) { + die "bts unclaim: use what claim token?\n"; + } + $claim = extractemail($claim); + bts_user("bugsquash\@qa.debian.org"); + bts_usertags("$bug", "-$claim"); +} + +=item B<severity> I<bug> I<severity> + +Change the I<severity> of a I<bug>. Available severities are: B<wishlist>, B<minor>, B<normal>, +B<important>, B<serious>, B<grave>, B<critical>. The severity may be abbreviated to any +unique substring. + +=cut + +sub bts_severity { + my $bug = checkbug(shift) + or die "bts severity: change the severity of what bug?\n"; + my $severity = lc(shift) + or die "bts severity: set \#$bug\'s severity to what?\n"; + my @matches = grep /^\Q$severity\E/i, @valid_severities; + if (@matches != 1) { + die +"bts severity: \"$severity\" is not a valid severity.\nChoose from: @valid_severities\n"; + } + opts_done(@_); + mailbts("severity of $bug is $matches[0]", "severity $bug $matches[0]"); +} + +=item B<forwarded> I<bug> I<address> + +Mark the I<bug> as forwarded to the given I<address> (usually an email address or +a URL for an upstream bug tracker). + +=cut + +sub bts_forwarded { + my $bug = checkbug(shift) + or die "bts forwarded: mark what bug as forwarded?\n"; + my $email = join(' ', @_); + if ($email =~ /$btsserver/) { + die +"bts forwarded: We don't forward bugs within $btsserver, use bts reassign instead\n"; + } + if (!length $email) { + die + "bts forwarded: mark bug $bug as forwarded to what email address?\n"; + } + mailbts("bug $bug is forwarded to $email", "forwarded $bug $email"); +} + +=item B<notforwarded> I<bug> + +Mark a I<bug> as not forwarded. + +=cut + +sub bts_notforwarded { + my $bug = checkbug(shift) or die "bts notforwarded: what bug?\n"; + opts_done(@_); + mailbts("bug $bug is not forwarded", "notforwarded $bug"); +} + +=item B<package> [I<package> ...] + +The following commands will only apply to bugs against the listed +I<package>s; this acts as a safety mechanism for the BTS. If no packages +are listed, this check is turned off again. + +=cut + +sub bts_package { + if (@_) { + bts_limit(map { "package:$_" } @_); + } else { + bts_limit('package'); + } +} + +=item B<limit> [I<key>[B<:>I<value>]] ... + +The following commands will only apply to bugs which meet the specified +criterion; this acts as a safety mechanism for the BTS. If no I<value>s are +listed, the limits for that I<key> are turned off again. If no I<key>s are +specified, all limits are reset. + +=over 8 + +=item B<submitter> + +E-mail address of the submitter. + +=item B<date> + +Date the bug was submitted. + +=item B<subject> + +Subject of the bug. + +=item B<msgid> + +Message-id of the initial bug report. + +=item B<package> + +Binary package name. + +=item B<source> + +Source package name. + +=item B<tag> + +Tags applied to the bug. + +=item B<severity> + +Bug severity. + +=item B<owner> + +Bug's owner. + +=item B<affects> + +Bugs affecting this package. + +=item B<archive> + +Whether to search archived bugs or normal bugs; defaults to B<0> +(i.e. only search normal bugs). As a special case, if archive is +B<both>, both archived and unarchived bugs are returned. + +=back + +For example, to limit the set of bugs affected by the subsequent control +commands to those submitted by jrandomdeveloper@example.com and tagged +B<wontfix>, one would use + +bts limit submitter:jrandomdeveloper@example.com tag:wontfix + +If a key is used multiple times then the set of bugs selected includes +those matching any of the supplied values; for example + +bts limit package:foo severity:wishlist severity:minor + +only applies the subsequent control commands to bugs of package foo with +either B<wishlist> or B<minor> severity. + +=cut + +sub bts_limit { + my @args = @_; + my %limits; + # Ensure we're using the limit fields that debbugs expects. These are the + # keys from Debbugs::Status::fields + my %valid_keys = ( + submitter => 'originator', + date => 'date', + subject => 'subject', + msgid => 'msgid', + package => 'package', + source => 'source', + src => 'source', + tag => 'keywords', + severity => 'severity', + owner => 'owner', + affects => 'affects', + archive => 'unarchived', + ); + for my $arg (@args) { + my ($key, $value) = split /:/, $arg, 2; + next unless $key; + if (!defined $value) { + die "bts limit: No value given for '$key'\n"; + } + if (exists $valid_keys{$key}) { + # Support "$key:" by making it look like "$key", i.e. no $value + # defined + undef $value unless length($value); + if ($key eq "archive") { + if (defined $value) { + # limit looks for unarchived, not archive. Verify we have + # a valid value and then switch the boolean value to match + # archive => unarchive + if ($value =~ /^yes|1|true|on$/i) { + $value = 0; + } elsif ($value =~ /^no|0|false|off$/i) { + $value = 1; + } elsif ($value ne 'both') { + die "bts limit: Invalid value ($value) for archive\n"; + } + } + } + $key = $valid_keys{$key}; + if (defined $value and $value) { + push(@{ $limits{$key} }, $value); + } else { + $limits{$key} = (); + } + } elsif ($key eq 'clear') { + %limits = (); + $limits{$key} = 1; + } else { + die "bts limit: Unrecognized key: $key\n"; + } + } + for my $key (keys %limits) { + if ($key eq 'clear') { + mailbts('clear all limit(s)', 'limit clear'); + next; + } + if (defined $limits{$key}) { + my $value = join ' ', @{ $limits{$key} }; + mailbts("limit $key to $value", "limit $key $value"); + } else { + mailbts("clear $key limit", "limit $key"); + } + } +} + +=item B<owner> I<bug> I<owner-email> + +Change the "owner" address of a I<bug>, with B<!> meaning +`use the address on the current email as the new owner address'. + +The owner of a bug accepts responsibility for dealing with it. + +=cut + +sub bts_owner { + my $bug = checkbug(shift) or die "bts owner: change owner of what bug?\n"; + my $owner = checkemail(shift, 1) + or die "bts owner: change owner to what?\n"; + opts_done(@_); + mailbts("owner $bug", "owner $bug $owner"); +} + +=item B<noowner> I<bug> + +Mark a bug as having no "owner". + +=cut + +sub bts_noowner { + my $bug = checkbug(shift) or die "bts noowner: what bug?\n"; + opts_done(@_); + mailbts("bug $bug has no owner", "noowner $bug"); +} + +=item B<subscribe> I<bug> [I<email>] + +Subscribe the given I<email> address to the specified I<bug> report. If no email +address is specified, the environment variable B<DEBEMAIL> or B<EMAIL> (in that +order) is used. If those are not set, or B<!> is given as email address, +your default address will be used. + +After executing this command, you will be sent a subscription confirmation to +which you have to reply. When subscribed to a bug report, you receive all +relevant emails and notifications. Use the unsubscribe command to unsubscribe. + +=cut + +sub bts_subscribe { + my $bug = checkbug(shift) or die "bts subscribe: subscribe to what bug?\n"; + my $email = checkemail(shift, 1); + $email = lc($email) if defined $email; + if (defined $email and $email eq '!') { $email = undef; } + else { + $email ||= $ENV{'DEBEMAIL'}; + $email ||= $ENV{'EMAIL'}; + $email = extractemail($email) if defined $email; + } + opts_done(@_); + mailto( + 'subscription request for bug #' . $bug, '', + $bug . '-subscribe@' . $btsserver, $email + ); +} + +=item B<unsubscribe> I<bug> [I<email>] + +Unsubscribe the given email address from the specified bug report. As with +subscribe above, if no email address is specified, the environment variables +B<DEBEMAIL> or B<EMAIL> (in that order) is used. If those are not set, or B<!> is +given as email address, your default address will be used. + +After executing this command, you will be sent an unsubscription confirmation +to which you have to reply. Use the B<subscribe> command to, well, subscribe. + +=cut + +sub bts_unsubscribe { + my $bug = checkbug(shift) + or die "bts unsubscribe: unsubscribe from what bug?\n"; + my $email = checkemail(shift, 1); + $email = lc($email) if defined $email; + if (defined $email and $email eq '!') { $email = undef; } + else { + $email ||= $ENV{'DEBEMAIL'}; + $email ||= $ENV{'EMAIL'}; + $email = extractemail($email) if defined $email; + } + opts_done(@_); + mailto( + 'unsubscription request for bug #' . $bug, '', + $bug . '-unsubscribe@' . $btsserver, $email + ); +} + +=item B<reportspam> I<bug> ... + +The B<reportspam> command allows you to report a I<bug> report as containing spam. +It saves one from having to go to the bug web page to do so. + +=cut + +sub bts_reportspam { + my @bugs; + + if (!have_lwp()) { + die "$progname: Couldn't run bts reportspam: $lwp_broken\n"; + } + + foreach (@_) { + my $bug = checkbug($_) + or die "bts reportspam: some bug number(s) not valid\n"; + push @bugs, $bug; + } + @bugs >= 1 + or die "bts reportspam: at least one bug number must be specified\n"; + + init_agent() unless $ua; + foreach my $bug (@bugs) { + my $url = "$btscgispamurl?bug=$bug;ok=ok"; + if ($noaction) { + print "bts reportspam: would report $bug as containing spam (URL: " + . $url . ")\n"; + } else { + my $request = HTTP::Request->new('GET', $url); + my $response = $ua->request($request); + if (!$response->is_success) { + warn "$progname: failed to report $bug as containing spam: " + . $response->status_line . "\n"; + } + } + } +} + +=item B<spamreport> I<bug> ... + +B<spamreport> is a synonym for B<reportspam>. + +=cut + +sub bts_spamreport { + goto &bts_reportspam; +} + +=item B<cache> [I<options>] [I<maint_email> | I<pkg> | B<src:>I<pkg> | B<from:>I<submitter>] + +=item B<cache> [I<options>] [B<release-critical> | B<release-critical/>... | B<RC>] + +Generate or update a cache of bug reports for the given email address +or package. By default it downloads all bugs belonging to the email +address in the B<DEBEMAIL> environment variable (or the B<EMAIL> environment +variable if B<DEBEMAIL> is unset). This command may be repeated to cache +bugs belonging to several people or packages. If multiple packages or +addresses are supplied, bugs belonging to any of the arguments will be +cached; those belonging to more than one of the arguments will only be +downloaded once. The cached bugs are stored in +F<$XDG_CACHE_HOME/devscripts/bts/> or, if B<XDG_CACHE_HOME> is not set, in +F<~/.cache/devscripts/bts/>. + +You can use the cached bugs with the B<-o> switch. For example: + + bts -o bugs + bts -o show 12345 + +Also, B<bts> will update the files in it in a piecemeal fashion as it +downloads information from the BTS using the B<show> command. You might +thus set up the cache, and update the whole thing once a week, while +letting the automatic cache updates update the bugs you frequently +refer to during the week. + +Some options affect the behaviour of the B<cache> command. The first is +the setting of B<--cache-mode>, which controls how much B<bts> downloads +of the referenced links from the bug page, including boring bits such +as the acknowledgement emails, emails to the control bot, and the mbox +version of the bug report. It can take three values: B<min> (the +minimum), B<mbox> (download the minimum plus the mbox version of the bug +report) or B<full> (the whole works). The second is B<--force-refresh> or +B<-f>, which forces the download, even if the cached bug report is +up-to-date. The B<--include-resolved> option indicates whether bug +reports marked as resolved should be downloaded during caching. + +Each of these is configurable from the configuration +file, as described below. They may also be specified after the +B<cache> command as well as at the start of the command line. + +Finally, B<-q> or B<--quiet> will suppress messages about caches being +up-to-date, and giving the option twice will suppress all cache +messages (except for error messages). + +Beware of caching RC, though: it will take a LONG time! (With 1000+ +RC bugs and a delay of 5 seconds between bugs, you're looking at a +minimum of 1.5 hours, and probably significantly more than that.) + +=cut + +sub bts_cache { + @ARGV = @_; + my ($sub_cachemode, $sub_refreshmode, $sub_updatemode); + my $sub_quiet = $quiet; + my $sub_includeresolved = $includeresolved; + GetOptions( + "cache-mode|cachemode=s" => \$sub_cachemode, + "f" => \$sub_refreshmode, + "force-refresh!" => \$sub_refreshmode, + "only-new!" => \$sub_updatemode, + "q|quiet+" => \$sub_quiet, + "include-resolved!" => \$sub_includeresolved, + ) or die "$progname: unknown options for cache command\n"; + @_ = @ARGV; # whatever's left + + if (defined $sub_refreshmode) { + ($refreshmode, $sub_refreshmode) = ($sub_refreshmode, $refreshmode); + } + if (defined $sub_updatemode) { + ($updatemode, $sub_updatemode) = ($sub_updatemode, $updatemode); + } + if (defined $sub_cachemode) { + if ($sub_cachemode =~ $cachemode_re) { + ($cachemode, $sub_cachemode) = ($sub_cachemode, $cachemode); + } else { + warn +"$progname: ignoring invalid --cache-mode $sub_cachemode;\nmust be one of min, mbox, full.\n"; + } + } + # This may be a no-op, we don't mind + ($quiet, $sub_quiet) = ($sub_quiet, $quiet); + ($includeresolved, $sub_includeresolved) + = ($sub_includeresolved, $includeresolved); + + prunecache(); + if (!have_lwp()) { + die "$progname: Couldn't run bts cache: $lwp_broken\n"; + } + + if (!-d $cachedir) { + my $err; + make_path($cachedir, { error => \$err }); + if (@$err) { + my ($path, $msg) = each(%{ $err->[0] }); + die "$progname: couldn't mkdir $path: $msg\n"; + } + } + + download("css/bugs.css"); + + my $tocache; + if (@_ > 0) { $tocache = sanitizething(shift); } + else { $tocache = ''; } + + if (!length $tocache) { + $tocache = $ENV{'DEBEMAIL'} || $ENV{'EMAIL'} || ''; + if ($tocache =~ /^.*\s<(.*)>\s*$/) { $tocache = $1; } + } + if (!length $tocache) { + die "bts cache: cache what?\n"; + } + + my $sub_thgopts = ''; + $sub_thgopts = ';pend-exc=done' + if (!$includeresolved && $tocache !~ /^release-critical/); + + my %bugs = (); + my %oldbugs = (); + + do { + %oldbugs = (%oldbugs, + map { $_ => 1 } bugs_from_thing($tocache, $sub_thgopts)); + + # download index + download($tocache, $sub_thgopts, 1); + + %bugs + = (%bugs, map { $_ => 1 } bugs_from_thing($tocache, $sub_thgopts)); + + $tocache = sanitizething(shift); + } while (defined $tocache); + + # remove old bugs from cache + if (keys %oldbugs) { + tie(%timestamp, "Devscripts::DB_File_Lock", $timestampdb, + O_RDWR() | O_CREAT(), + 0600, $DB_HASH, "write") + or die + "$progname: couldn't open DB file $timestampdb for writing: $!\n" + if !tied %timestamp; + } + + foreach my $bug (keys %oldbugs) { + if (!$bugs{$bug}) { + deletecache($bug); + } + } + + untie %timestamp; + + # download bugs + my $bugcount = 1; + my $bugtotal = scalar keys %bugs; + foreach my $bug (keys %bugs) { + if (-f cachefile($bug, '') and $updatemode) { + print "Skipping $bug as requested ... $bugcount/$bugtotal\n" + if !$quiet; + $bugcount++; + next; + } + download($bug, '', 1, 0, $bugcount, $bugtotal); + sleep $opt_cachedelay; + $bugcount++; + } + + # revert options + if (defined $sub_refreshmode) { + $refreshmode = $sub_refreshmode; + } + if (defined $sub_updatemode) { + $updatemode = $sub_updatemode; + } + if (defined $sub_cachemode) { + $cachemode = $sub_cachemode; + } + $quiet = $sub_quiet; + $includeresolved = $sub_includeresolved; +} + +=item B<cleancache> I<package> | B<src:>I<package> | I<maintainer> + +=item B<cleancache from:>I<submitter> | B<tag:>I<tag> | B<usertag:>I<tag> | I<number> | B<ALL> + +Clean the cache for the specified I<package>, I<maintainer>, etc., as +described above for the B<bugs> command, or clean the entire cache if +B<ALL> is specified. This is useful if you are going to have permanent +network access or if the database has become corrupted for some +reason. Note that for safety, this command does not default to the +value of B<DEBEMAIL> or B<EMAIL>. + +=cut + +sub bts_cleancache { + prunecache(); + my $toclean = sanitizething(shift); + if (!defined $toclean) { + die "bts cleancache: clean what?\n"; + } + if (!-d $cachedir) { + return; + } + if ($toclean eq 'ALL') { + if (system("/bin/rm", "-rf", $cachedir) >> 8 != 0) { + warn "Problems cleaning cache: $!\n"; + } + return; + } + + # clean index + tie(%timestamp, "Devscripts::DB_File_Lock", $timestampdb, + O_RDWR() | O_CREAT(), + 0600, $DB_HASH, "write") + or die "$progname: couldn't open DB file $timestampdb for writing: $!\n" + if !tied %timestamp; + + if ($toclean =~ /^\d+$/) { + # single bug only + deletecache($toclean); + } else { + my @bugs_to_clean = bugs_from_thing($toclean); + deletecache($toclean); + + # remove old bugs from cache + foreach my $bug (@bugs_to_clean) { + deletecache($bug); + } + } + + untie %timestamp; +} + +=item B<listcachedbugs> [I<number>] + +List cached bug ids (intended to support bash completion). The optional number argument +restricts the list to those bug ids that start with that number. + +=cut + +sub bts_listcachedbugs { + my $number = shift; + if (not defined $number) { + $number = ''; + } + if ($number =~ m{\D}) { + return; + } + my $untie = 0; + if (not tied %timestamp) { + tie(%timestamp, "Devscripts::DB_File_Lock", $timestampdb, + O_RDONLY(), 0600, $DB_HASH, "read") + or die + "$progname: couldn't open DB file $timestampdb for reading: $!\n"; + $untie = 1; + } + + print join "\n", grep { $_ =~ m{^$number\d+$} } sort keys %timestamp; + print "\n"; + + if ($untie) { + untie %timestamp; + } +} + +# Add any new commands here. + +=item B<version> + +Display version and copyright information. + +=cut + +sub bts_version { + print <<"EOF"; +$progname version $version +Copyright (C) 2001-2003 by Joey Hess <joeyh\@debian.org>. +Modifications Copyright (C) 2002-2004 by Julian Gilbey <jdg\@debian.org>. +Modifications Copyright (C) 2007 by Josh Triplett <josh\@freedesktop.org>. +It is licensed under the terms of the GPL, either version 2 of the +License, or (at your option) any later version. +EOF +} + +=item B<help> + +Display a short summary of commands, suspiciously similar to parts of this +man page. + +=cut + +# Other supporting subs + +# This must be the last bts_* sub +sub bts_help { + my $inlist = 0; + my $insublist = 0; + print <<"EOF"; +Usage: $progname [options] command [args] [\#comment] [.|, command ... ] +Valid options are: + -o, --offline Do not attempt to connect to BTS for show/bug + commands: use cached copy + --online, --no-offline Attempt to connect (default) + -n, --no-action Do not send emails but print them to standard output. + --no-cache Do not attempt to cache new versions of BTS + pages when performing show/bug commands + --cache Do attempt to cache new versions of BTS + pages when performing show/bug commands (default) + --cache-mode={min|mbox|full} + How much to cache when we are caching: the sensible + bare minimum (default), the mbox as well, or + everything? + --cache-delay=seconds Time to sleep between each download when caching. + -m, --mbox With show or bugs, open a mailreader to read the mbox + version instead + --mailreader=CMD Run CMD to read an mbox; default is 'mutt -f %s' + (must contain %s, which is replaced by mbox name) + --cc-addr=CC_EMAIL_ADDRESS + Send carbon copies to a list of users. + CC_EMAIL_ADDRESS should be a comma-separated list of + e-mail addresses. Multiple options add more CCs. + --use-default-cc Send carbon copies to any addresses specified in the + configuration file BTS_DEFAULT_CC (default) + --no-use-default-cc Do not do so + --sendmail=cmd Sendmail command to use (default /usr/sbin/sendmail) + --mutt Use mutt for sending of mails. + --no-mutt Do not do so (default) + --smtp-host=host SMTP host to use + --smtp-username=user } Credentials to use when connecting to an SMTP + --smtp-password=pass } server which requires authentication + --smtp-helo=helo HELO to use when connecting to the SMTP server; + (defaults to the content of /etc/mailname) + --bts-server The name of the debbugs server to use + (default https://bugs.debian.org) + -f, --force-refresh Reload all bug reports being cached, even unchanged + ones + --no-force-refresh Do not do so (default) + --only-new Download only new bugs when caching. Do not check + for updates in bugs we already have. + --include-resolved Cache bugs marked as resolved (default) + --no-include-resolved Do not cache bugs marked as resolved + --no-ack Suppress BTS acknowledgment mails + --ack Do not do so (default) + -i, --interactive Prompt for confirmation before sending e-mail + --force-interactive Same as --interactive, with the exception that an + editor is spawned before confirmation is requested + --no-interactive Do not do so (default) + -q, --quiet Only display information about newly cached pages. + If given twice, only display error messages. + --no-conf, --noconf Do not read devscripts config files; + must be the first option given + -h, --help Display this message + -v, --version Display version and copyright info + +Default settings modified by devscripts configuration files: +$modified_conf_msg + +Valid commands are: +EOF + seek DATA, 0, 0; + while (<DATA>) { + $inlist = 1 if /^=over 4/; + next unless $inlist; + $insublist = 1 if /^=over [^4]/; + $insublist = 0 if /^=back/; + if (/^=item\sB<([^->].*)>/ and !$insublist) { + if ($1 eq 'help') { + last; + } + # Strip POD markup before displaying and ensure we don't wrap + # longer lines + my $parser = Pod::BTS->new(width => 100); + $parser->no_whining(1); + $parser->output_fh(\*STDOUT); + $parser->parse_string_document($_); + } + } +} + +# Strips any leading # or Bug# and trailing : from a thing if what's left is +# a pure positive number; +# also RC is a synonym for release-critical/other/all.html +sub sanitizething { + my $bug = $_[0]; + defined $bug or return undef; + + return 'release-critical/other/all.html' if $bug eq 'RC'; + return 'release-critical/index.html' if $bug eq 'release-critical'; + $bug =~ s/^(?:(?:Bug)?\#)?(\d+):?$/$1/; + return $bug; +} + +# Perform basic validation of an argument which should be an email address, +# handling ! if allowed +sub checkemail { + my $email = $_[0] or return; + my $allowbang = $_[1]; + + if ($email !~ /\@/ && (!$allowbang || $email ne '!')) { + return; + } + + return $email; +} + +# Validate a bug number. Strips out extraneous leading junk, allowing +# for things like "#74041" and "Bug#94921" +sub checkbug { + my $bug = $_[0] or return ""; + my $quiet = $_[1] || 0; # used when we don't want warnings from checkbug + + if ($bug eq 'it') { + if (not defined $it) { + die +"$progname: You specified 'it', but no previous bug number referenced!\n"; + } + } else { + $bug =~ s/^(?:(?:bug)?\#)?(-?\d+):?$/$1/i; + if (!exists $clonedbugs{$bug} + && (!length $bug || $bug !~ /^[0-9]+$/)) { + warn "\"$_[0]\" does not look like a bug number\n" unless $quiet; + return ""; + } + + # Valid, now set $it to this so that we can refer to it by 'it' later + $it = $bug; + } + + return $it; +} + +# Stores up some extra information for a mail to the bts. +sub mailbts { + if ($subject eq '') { + $subject = $_[0]; + } elsif (length($subject) + length($_[0]) < 100) { + $subject .= ", $_[0]" if length($_[0]); + } elsif ($subject !~ / ...$/) { + $subject .= " ..."; + } + $body .= "$comment[$index]\n" if $comment[$index]; + $body .= "$_[1]\n"; +} + +# Extract an array of email addresses from a string +sub extract_addresses { + my $s = shift; + my @addresses; + + # Original regular expression from git-send-email, slightly modified + while ($s and $s =~ /([^,<>"\s\@]+\@[^.,<>"\s@]+(?:\.[^.,<>"\s\@]+)+)(.*)/) + { + push @addresses, $1; + $s = $2; + } + return @addresses; +} + +# Send one full mail message using the smtphost or sendmail. +sub send_mail { + my ($from, $to, $cc, $subject, $body) = @_; + + my @fromaddresses = extract_addresses($from); + unless (@fromaddresses) { + die "Something went wrong: no from address" unless $noaction; + @fromaddresses = ($from = '<undefined>'); + } + my $fromaddress = $fromaddresses[0]; + # Message-ID algorithm from git-send-email + my $msgid + = sprintf("%s-%s", time(), int(rand(4200))) . "-bts-$fromaddress"; + my $date = strftime "%a, %d %b %Y %T %z", localtime; + + my $message = fold_from_header("From: $from") . "\n"; + $message .= "To: $to\n" if length $to; + $message .= "Cc: $cc\n" if length $cc; + $message .= "X-Debbugs-No-Ack: Yes\n" if not $requestack; + $message + .= "Subject: $subject\n" + . "Date: $date\n" + . "User-Agent: devscripts bts/$version$toolname\n" + . "Message-ID: <$msgid>\n" . "\n"; + + $body = addfooter($body); + ($message, $body) = confirmmail($message, $body); + + return if not defined $body; + + $message .= "$body\n"; + if ($noaction) { + print "$message\n"; + } elsif ($use_mutt) { + my ($fh, $filename) = tempfile( + "btsXXXXXX", + SUFFIX => ".mail", + DIR => File::Spec->tmpdir, + UNLINK => 1 + ); + open(MAILOUT, ">&", $fh) + or die "$progname: writing to temporary file: $!\n"; + + print MAILOUT $message; + + my $mailcmd = $muttcmd; + $mailcmd =~ s/\%([%s])/$1 eq '%' ? '%' : $filename/eg; + + exec($mailcmd) or die "$progname: unable to start mailclient: $!"; + } elsif (length $smtphost) { + my $smtp; + + if ($smtphost =~ m%^(?:(?:ssmtp|smtps)://)(.*)$%) { + my ($host, $port) = split(/:/, $1); + $port ||= '465'; + + $smtp = Net::SMTP->new( + $host, + Port => $port, + Hello => $smtphelo, + SSL => 1, + ) + or die +"$progname: failed to open SMTP connection with TLS to $smtphost\n($@)\n"; + } else { + my ($host, $port) = split(/:/, $smtphost); + $port ||= '25'; + + $smtp = Net::SMTP->new( + $host, + Port => $port, + Hello => $smtphelo, + ) + or die + "$progname: failed to open SMTP connection to $smtphost\n($@)\n"; + } + if ($smtpuser) { + if (have_authen_sasl) { + $smtppass = getpass() if not $smtppass; + # Enforce STARTTLS; Net::SMTP will otherwise refuse auth() in + # the next step, and terminate the connection with FIN. + $smtp->starttls() + or die "$progname: Could not upgrade with STARTTLS"; + $smtp->auth($smtpuser, $smtppass) + or die + "$progname: failed to authenticate to $smtphost\n($@)\n"; + } else { + die +"$progname: failed to authenticate to $smtphost: $authen_sasl_broken\n"; + } + } + $smtp->mail($fromaddress) + or die + "$progname: failed to set SMTP from address $fromaddress\n($@)\n"; + my @addresses = extract_addresses($to); + push @addresses, extract_addresses($cc); + foreach my $address (@addresses) { + $smtp->recipient($address) + or die + "$progname: failed to set SMTP recipient $address\n($@)\n"; + } + $smtp->data($message) + or die "$progname: failed to send message as SMTP DATA\n($@)\n"; + $smtp->quit + or die "$progname: failed to quit SMTP connection\n($@)\n"; + } else { + my $pid = open(MAIL, "|-"); + if (!defined $pid) { + die "$progname: Couldn't fork: $!\n"; + } + $SIG{'PIPE'} = sub { die "$progname: pipe for $sendmailcmd broke\n"; }; + if ($pid) { + # parent + print MAIL $message; + close MAIL or die "$progname: $sendmailcmd error: $!\n"; + } else { + # child + if ($debug) { + exec("/bin/cat") + or die "$progname: error running cat: $!\n"; + } else { + my @mailcmd = split ' ', $sendmailcmd; + push @mailcmd, "-t" if $sendmailcmd =~ /$sendmail_t/; + exec @mailcmd + or die "$progname: error running $sendmailcmd: $!\n"; + } + } + } +} + +sub generate_packages_cc { + my @ccs; + if (keys %ccpackages && $packagesserver) { + push @ccs, map { "$_\@$packagesserver" } sort keys %ccpackages; + } + if (keys %ccsubmitters && $btsserver) { + push @ccs, map { "$_\@$btsserver" } sort keys %ccsubmitters; + } + return @ccs; +} + +# Sends all cached mail to the bts (duh). +sub mailbtsall { + my $subject = shift; + my $body = shift; + + my $charset = `locale charmap`; + chomp $charset; + $charset =~ s/^ANSI_X3\.4-19(68|86)$/US-ASCII/; + $subject = MIME_encode_mimewords($subject, 'Charset' => $charset); + + if ($interactive eq 'force' || $use_mutt) { + push @ccemails, generate_packages_cc(); + } + if ($ccsecurity) { + push @ccemails, $ccsecurity; + } + my $ccemail = join(', ', @ccemails); + if ($ENV{'DEBEMAIL'} || $ENV{'EMAIL'}) { + # We need to fake the From: line + my ($email, $name); + if (exists $ENV{'DEBFULLNAME'}) { $name = $ENV{'DEBFULLNAME'}; } + if (exists $ENV{'DEBEMAIL'}) { + $email = $ENV{'DEBEMAIL'}; + if ($email =~ /^(.*?)\s+<(.*)>\s*$/) { + $name ||= $1; + $email = $2; + } + } + if (exists $ENV{'EMAIL'}) { + if ($ENV{'EMAIL'} =~ /^(.*?)\s+<(.*)>\s*$/) { + $name ||= $1; + $email ||= $2; + } else { + $email ||= $ENV{'EMAIL'}; + } + } + if (!$name) { + # Perhaps not ideal, but it will have to do + $name = (getpwuid($<))[6]; + $name =~ s/,.*//; + } + my $from = $name ? "$name <$email>" : $email; + $from = MIME_encode_mimewords($from, 'Charset' => $charset); + + send_mail($from, $btsemail, $ccemail, $subject, $body); + } else { # No DEBEMAIL + my $header = ""; + + $header = "To: $btsemail\n"; + $header .= "Cc: $ccemail\n" if length $ccemail; + $header .= "X-Debbugs-No-Ack: Yes\n" if not $requestack; + $header .= "Subject: $subject\n" + . "User-Agent: devscripts bts/$version$toolname\n" . "\n"; + + $body = addfooter($body); + ($header, $body) = confirmmail($header, $body); + + return if not defined $body; + + if ($noaction) { + print "$header$body\n"; + return; + } + + my $pid = open(MAIL, "|-"); + if (!defined $pid) { + die "$progname: Couldn't fork: $!\n"; + } + $SIG{'PIPE'} = sub { die "$progname: pipe for $sendmailcmd broke\n"; }; + if ($pid) { + # parent + print MAIL $header; + print MAIL $body; + close MAIL or die "$progname: $sendmailcmd: $!\n"; + } else { + # child + if ($debug) { + exec("/bin/cat") + or die "$progname: error running cat: $!\n"; + } else { + my @mailcmd = split ' ', $sendmailcmd; + push @mailcmd, "-t" if $sendmailcmd =~ /$sendmail_t/; + exec @mailcmd + or die "$progname: error running $sendmailcmd: $!\n"; + } + } + } +} + +sub confirmmail { + my ($header, $body) = @_; + + return ($header, $body) if $noaction; + + $body = edit($body) if $interactive eq 'force'; + my $setHeader = 0; + if ($interactive ne 'no') { + while (1) { + print "\n", $header, "\n", $body, "\n---\n"; + print "OK to send? [Y/n/e] "; + $_ = <STDIN>; + if (/^n/i) { + $body = undef; + last; + } elsif (/^(y|$)/i) { + last; + } elsif (/^e/i) { + # Since the user has chosen to edit the message, we go ahead + # and add the $ccpackages Ccs (if they haven't already been + # added due to interactive). + if ($interactive ne 'force' && !$setHeader) { + $setHeader = 1; + my @ccemails = generate_packages_cc(); + my $ccs = join(', ', @ccemails); + if ($header =~ m/^Cc: (.*?)$/m) { + $ccs = "$1, $ccs"; + $header =~ s/^Cc: .*?$/Cc: $ccs/m; + } else { + $header =~ s/^(To: .*?)$/$1\nCc: $ccs/m; + } + } + $body = edit($body); + } + } + } + + return ($header, $body); +} + +sub addfooter() { + my $body = shift; + + $body .= "thanks\n"; + if ($interactive eq 'force') { + if (-r $ENV{'HOME'} . "/.signature") { + if (open SIG, "<", $ENV{'HOME'} . "/.signature") { + $body .= "-- \n"; + while (<SIG>) { + $body .= $_; + } + close SIG; + } + } + } + + return $body; +} + +sub getpass() { + system "stty -echo cbreak </dev/tty"; + die "$progname: error disabling stty echo\n" if $?; + print "\a${smtpuser}"; + print "\@$smtphost" if $smtpuser !~ /\@/; + print "'s SMTP password: "; + $_ = <STDIN>; + chomp; + print "\n"; + system "stty echo -cbreak </dev/tty"; + die "$progname: error enabling stty echo\n" if $?; + return $_; +} + +sub extractemail() { + my $thing = shift or die "$progname: extract e-mail from what?\n"; + + if ($thing =~ /^(.*?)\s+<(.*)>\s*$/) { + $thing = $2; + } + + return $thing; +} + +# A simplified version of mailbtsall which sends one message only to +# a specified address using the specified email From: header +sub mailto { + my ($subject, $body, $to, $from) = @_; + + if (defined($from) || $noaction) { + send_mail($from, $to, '', $subject, $body); + } else { # No $from + unless (system("command -v mailx >/dev/null 2>&1") == 0) { + die +"$progname: You need to either specify an email address (say using DEBEMAIL)\nor have the bsd-mailx package (or another package providing mailx) installed\nto send mail!\n"; + } + my $pid = open(MAIL, "|-"); + if (!defined $pid) { + die "$progname: Couldn't fork: $!\n"; + } + $SIG{'PIPE'} = sub { die "$progname: pipe for mailx broke\n"; }; + if ($pid) { + # parent + print MAIL $body; + close MAIL or die "$progname: mailx: $!\n"; + } else { + # child + if ($debug) { + exec("/bin/cat") + or die "$progname: error running cat: $!\n"; + } else { + exec("mailx", "-s", $subject, $to) + or die "$progname: error running mailx: $!\n"; + } + } + } +} + +# The following routines are taken from a patched version of MIME::Words +# posted at http://mail.nl.linux.org/linux-utf8/2002-01/msg00242.html +# by Richard Čepas (Chepas) <rch@richard.eu.org> + +sub MIME_encode_B { + my $str = shift; + require MIME::Base64; + MIME::Base64::encode_base64($str, ''); +} + +sub MIME_encode_Q { + my $str = shift; + $str + =~ s{([_\?\=\015\012\t $NONPRINT])}{$1 eq ' ' ? '_' : sprintf("=%02X", ord($1))}eog + ; # RFC-2047, Q rule 3 + $str; +} + +sub MIME_encode_mimeword { + my $word = shift; + my $encoding = uc(shift || 'Q'); + my $charset = uc(shift || 'ISO-8859-1'); + my $encfunc = (($encoding eq 'Q') ? \&MIME_encode_Q : \&MIME_encode_B); + "=?$charset?$encoding?" . &$encfunc($word) . "?="; +} + +sub MIME_encode_mimewords { + my ($rawstr, %params) = @_; + # check if we have something to encode + $rawstr !~ /[$NONPRINT]/o and $rawstr !~ /\=\?/o and return $rawstr; + my $charset = $params{Charset} || 'ISO-8859-1'; + # if there is 1/3 unsafe bytes, the Q encoded string will be 1.66 times + # longer and B encoded string will be 1.33 times longer than original one + my $encoding = lc( + $params{Encoding} + || ( + length($rawstr) > 3 * ($rawstr =~ tr/[\x00-\x1F\x7F-\xFF]//) + ? 'q' + : 'b' + )); + + # Encode any "words" with unsafe bytes. + my ($last_token, $last_word_encoded, $token) = ('', 0); + $rawstr =~ s{([^\015\012\t ]+|[\015\012\t ]+)}{ # get next "word" + $token = $1; + if ($token =~ /[\015\012\t ]+/) { # white-space + $last_token = $token; + } else { + if ($token !~ /[$NONPRINT]/o and $token !~ /\=\?/o) { + # no unsafe bytes, leave as it is + $last_word_encoded = 0; + $last_token = $token; + } else { + # has unsafe bytes, encode to one or more encoded words + # white-space between two encoded words is skipped on + # decoding, so we should encode space in that case + $_ = $last_token =~ /[\015\012\t ]+/ && $last_word_encoded ? $last_token.$token : $token; + # We limit such words to about 18 bytes, to guarantee that the + # worst-case encoding give us no more than 54 + ~10 < 75 bytes + s{(.{1,15}[\x80-\xBF]{0,4})}{ + # don't split multibyte characters - this regexp should + # work for UTF-8 characters + MIME_encode_mimeword($1, $encoding, $charset).' '; + }sxeg; + $_ = substr($_, 0, -1); # remove trailing space + $last_word_encoded = 1; + $last_token = $token; + $_; + } + } + }sxeg; + $rawstr; +} + +# This is a stripped-down version of Mail::Header::_fold_line, but is +# not as general-purpose as the original, so take care if using it elsewhere! +# The heuristics are changed to prevent splitting in the middle of an +# encoded word; we should not have any commas or semicolons! +sub fold_from_header { + my $header = shift; + chomp $header; # We assume there wasn't a newline anyhow + + my $maxlen = 76; + my $max = int($maxlen - 5); # 4 for leading spcs + 1 for [\,\;] + + if (length($header) > $maxlen) { + # Split the line up: + # first split at a whitespace, + # else we are looking at a single word and we won't try to split + # it, even though we really ought to + # But this could only happen if someone deliberately uses a really + # long name with no spaces in it. + my @x; + + push @x, $1 + while ( + $header =~ s/^\s* + ([^\"]{1,$max}\s + |[^\s\"]*(?:\"[^\"]*\"[ \t]?[^\s\"]*)+\s + |[^\s\"]+\s + ) + //x + ); + push @x, $header; + map { s/\s*$// } @x; + if (@x > 1 and length($x[-1]) + length($x[-2]) < $max) { + $x[-2] .= " $x[-1]"; + pop @x; + } + $x[0] =~ s/^\s*//; + $header = join("\n ", @x); + } + + $header =~ s/^(\S+)\n\s*(?=\S)/$1 /so; + return $header; +} + +########## Browsing and caching subroutines + +# Mirrors a given thing; if the online version is no newer than our +# cached version, then returns an empty string, otherwise returns the +# live thing as a (non-empty) string +sub download { + my $thing = shift; + my $thgopts = shift || ''; + my $manual = shift; # true="bts cache", false="bts show/bug" + my $mboxing = shift; # true="bts --mbox show/bugs", and only if $manual=0 + my $bug_current = shift; # current bug being downloaded if caching + my $bug_total = shift; # total things to download if caching + my $timestamp = 0; + my $versionstamp = ''; + my $url; + + my $oldcwd = getcwd; + + # What URL are we to download? + if ($thgopts ne '') { + # have to be intelligent here :/ + $url = thing_to_url($thing) . $thgopts; + } else { + # let the BTS be intelligent + $url = "$btsurl$thing"; + } + + if (!-d $cachedir) { + die "$progname: download() called but no cachedir!\n"; + } + + chdir($cachedir) || die "$progname: chdir $cachedir: $!\n"; + + if (-f cachefile($thing, $thgopts)) { + ($timestamp, $versionstamp) = get_timestamp($thing, $thgopts); + $timestamp ||= 0; + $versionstamp ||= 0; + # And ensure we preserve any manual setting + if (is_manual($timestamp)) { $manual = 1; } + } + + # do we actually have to do more than we might have thought? + # yes, if we've caching with --cache-mode=mbox or full and the bug had + # previously been cached in a less thorough format + my $forcedownload = 0; + if ($thing =~ /^\d+$/ and !$refreshmode) { + if (old_cache_format_version($versionstamp)) { + $forcedownload = 1; + } elsif ($cachemode ne 'min' or $mboxing) { + if (!-r mboxfile($thing)) { + $forcedownload = 1; + } elsif ($cachemode eq 'full' and -d $thing) { + opendir DIR, $thing + or die "$progname: opendir $cachedir/$thing: $!\n"; + my @htmlfiles = grep { /^\d+\.html$/ } readdir(DIR); + closedir DIR; + $forcedownload = 1 unless @htmlfiles; + } + } + } + + print "Downloading $url ... " + if !$quiet + and $manual + and $thing ne "css/bugs.css"; + IO::Handle::flush(\*STDOUT); + my ($ret, $msg, $livepage, $contenttype) + = bts_mirror($url, $timestamp, $forcedownload); + my $charset = $contenttype || ''; + if ($charset =~ m/charset=(.*?)(;|\Z)/) { + $charset = $1; + } else { + $charset = ""; + } + if ($ret == MIRROR_UP_TO_DATE) { + # we have an up-to-date version already, nothing to do + # and $timestamp is guaranteed to be well-defined + if (is_automatic($timestamp) and $manual) { + set_timestamp($thing, $thgopts, make_manual($timestamp), + $versionstamp); + } + + if (!$quiet and $manual and $thing ne "css/bugs.css") { + print "(cache already up-to-date) "; + print "$bug_current/$bug_total" if $bug_total; + print "\n"; + } + chdir $oldcwd or die "$progname: chdir $oldcwd failed: $!\n"; + return ""; + } elsif ($ret == MIRROR_DOWNLOADED) { + # Note the current timestamp, but don't record it until + # we've successfully stashed the data away + $timestamp = time; + + die "$progname: empty page downloaded\n" unless length $livepage; + + my $bug2filename = {}; + + if ($thing =~ /^\d+$/) { + # we've downloaded an individual bug, and it's been updated, + # so we need to also download all the attachments + $bug2filename + = download_attachments($thing, $livepage, $timestamp); + } + + my $data = $livepage; # work on a copy, not the original + my $cachefile = cachefile($thing, $thgopts); + open(OUT_CACHE, ">$cachefile") + or die "$progname: open $cachefile: $!\n"; + + $data = mangle_cache_file($data, $thing, $bug2filename, $timestamp, + $charset ? $contenttype : ''); + print OUT_CACHE $data; + close OUT_CACHE + or die "$progname: problems writing to $cachefile: $!\n"; + + set_timestamp($thing, $thgopts, + $manual ? make_manual($timestamp) : make_automatic($timestamp), + $version); + + if (!$quiet and $manual and $thing ne "css/bugs.css") { + print "(cached new version) "; + print "$bug_current/$bug_total" if $bug_total; + print "\n"; + } elsif ($quiet == 1 and $manual and $thing ne "css/bugs.css") { + print "Downloading $url ... (cached new version)\n"; + } elsif ($quiet > 1) { + # do nothing + } + + # Add a <base> tag to the live page content, so that relative urls + # in it work when it's passed to the web browser. + my $base = $url; + $base =~ s%/[^/]*$%%; + $livepage =~ s%<head>%<head><base href="$base">%i; + + chdir $oldcwd or die "$progname: chdir $oldcwd failed: $!\n"; + return $livepage; + } else { + die "$progname: couldn't download $url:\n$msg\n"; + } +} + +sub download_attachments { + my ($thing, $toppage, $timestamp) = @_; + my %bug2filename; + + # We search for appropriate strings in the top page, and save the + # attachments in files with names as follows: + # - if the attachment specifies a filename, save as bug#/msg#-att#/filename + # - if not, save as bug#/msg#-att# with suffix .txt if plain/text and + # .html if plain/html, no suffix otherwise (too much like hard work!) + # Since messages are never modified retrospectively, we don't download + # attachments which have already been downloaded + + # Yuck, yuck, yuck. This regex splits the $data string at every + # occurrence of either "[<a " or plain "<a ", preserving any "[". + my @data = split /(?:(?=\[<[Aa]\s)|(?<!\[)(?=<[Aa]\s))/, $toppage; + foreach (@data) { + next + unless +m%<a(?: class=\".*?\")? href="(?:/cgi(?:-bin)?/)?((bugreport\.cgi[^\"]+)"(?: .*?)?>|(version\.cgi[^\"]+)"><img[^>]* src="(?:/cgi(?:-bin)?/)?([^\"]+)">|(version\.cgi[^\"]+)">)%i; + + my $ref = $5; + $ref = $4 if not defined $ref; + $ref = $2 if not defined $ref; + + my ($msg, $filename) = href_to_filename($_); + + next unless defined $msg; + + if ($msg =~ /^\d+-\d+$/) { + # it's an attachment, must download + + if (-f dirname($filename)) { + warn +"$progname: found file where directory expected; using existing file (" + . dirname($filename) . ")\n"; + $bug2filename{$msg} = dirname($filename); + } else { + $bug2filename{$msg} = $filename; + } + + # already downloaded? + next if -f $bug2filename{$msg} and not $refreshmode; + } elsif ($cachemode eq 'full' and $msg =~ /^\d+$/) { + $bug2filename{$msg} = $filename; + # already downloaded? + next if -f $bug2filename{$msg} and not $refreshmode; + } elsif ($cachemode eq 'full' and $msg =~ /^\d+-mbox$/) { + $bug2filename{$msg} = $filename; + # already downloaded? + next if -f $bug2filename{$msg} and not $refreshmode; + } elsif (($cachemode eq 'full' or $cachemode eq 'mbox' or $mboxmode) + and $msg eq 'mbox') { + $bug2filename{$msg} = $filename; + # This always needs refreshing, as it does change as the bug + # changes + } elsif ($cachemode eq 'full' and $msg =~ /^(status|raw)mbox$/) { + $bug2filename{$msg} = $filename; + # Always need refreshing, as they could change each time the + # bug does + } elsif ($cachemode eq 'full' and $msg eq 'versions') { + $bug2filename{$msg} = $filename; + # Ensure we always download the full size images for + # version graphs, without the informational links + $ref =~ s%;info=1%;info=0%; + $ref =~ s%(;|\?)(height|width)=\d+%$1%g; + # already downloaded? + next if -f $bug2filename{$msg} and not $refreshmode; + } + + next unless exists $bug2filename{$msg}; + + warn "bts debug: downloading $btscgiurl$ref\n" if $debug; + init_agent() unless $ua; # shouldn't be necessary, but do just in case + my $request = HTTP::Request->new('GET', $btscgiurl . $ref); + my $response = $ua->request($request); + if ($response->is_success) { + my $content_length + = defined $response->content ? length($response->content) : 0; + if ($content_length == 0) { + warn + "$progname: failed to download $ref (length 0), skipping\n"; + next; + } + + my $data = $response->content; + + if ($msg =~ /^\d+$/) { + # we're dealing with a boring message, and so we must be + # in 'full' mode + $data =~ s%<HEAD>%<HEAD><BASE href="../">%; + $data = mangle_cache_file($data, $thing, 'full', $timestamp); + } + make_path(dirname($bug2filename{$msg})); + open OUT_CACHE, ">$bug2filename{$msg}" + or die "$progname: open cache $bug2filename{$msg}\n"; + print OUT_CACHE $data; + close OUT_CACHE; + } else { + my $status = $response->status_line; + warn "$progname: failed to download $ref ($status), skipping\n"; + next; + } + } + + return \%bug2filename; +} + +# Download the mailbox for a given bug, return mbox ($fh, filename) on success, +# die on failure +sub download_mbox { + my $thing = shift; + my $temp = shift; # do we wish to store it in cache or in a temp file? + my $mboxfile = mboxfile($thing); + + die "$progname: trying to download mbox for illegal bug number $thing.\n" + unless $mboxfile; + + if (!have_lwp()) { + die "$progname: couldn't run bts --mbox: $lwp_broken\n"; + } + init_agent() unless $ua; + + my $request = HTTP::Request->new('GET', + $btscgiurl . "bugreport.cgi?bug=$thing;mboxmaint=yes"); + my $response = $ua->request($request); + if ($response->is_success) { + my $content_length + = defined $response->content ? length($response->content) : 0; + if ($content_length == 0) { + die "$progname: failed to download mbox (length 0).\n"; + } + + my ($fh, $filename); + if ($temp) { + ($fh, $filename) = tempfile( + "btsXXXXXX", + SUFFIX => ".mbox", + DIR => File::Spec->tmpdir, + UNLINK => 1 + ); + # Use filehandle for security + open(OUT_MBOX, ">&", $fh) + or die "$progname: writing to temporary file: $!\n"; + } else { + $filename = $mboxfile; + open(OUT_MBOX, ">$mboxfile") + or die "$progname: writing to mbox file $mboxfile: $!\n"; + } + print OUT_MBOX $response->content; + close OUT_MBOX; + + return ($fh, $filename); + } else { + my $status = $response->status_line; + die "$progname: failed to download mbox ($status).\n"; + } +} + +# Mangle downloaded file to work in the local cache, so +# selectively modify the links +sub mangle_cache_file { + my ($data, $thing, $bug2filename, $timestamp, $ctype) = @_; + my $fullmode = !ref $bug2filename; + + # Undo unnecessary '+' encoding in URLs + while ($data =~ s!(href=\"[^\"]*)\%2b!$1+!ig) { } + my $time = localtime(abs($timestamp)); + $data + =~ s%(<BODY.*>)%$1<p><em>[Locally cached on $time by devscripts version $version]</em></p>%i; + $data =~ s%href="/css/bugs.css"%href="bugs.css"%; + if ($ctype) { + $data + =~ s%(<HEAD.*>)%$1<META HTTP-EQUIV="Content-Type" CONTENT="$ctype">%i; + } + + my @data; + # We have to distinguish between release-critical pages and normal BTS + # pages as they have a different structure + if ($thing =~ /^release-critical/) { + @data = split /(?=<[Aa])/, $data; + foreach (@data) { +s%<a href="(https?://$btsserver/cgi(?:-bin)?/bugreport\.cgi.*bug=(\d+)[^\"]*)">(.+?)</a>%<a href="$2.html">$3</a> (<a href="$1">online</a>)%i; +s%<a href="(https?://$btsserver/cgi(?:-bin)?/pkgreport\.cgi.*pkg=([^\"&;]+)[^\"]*)">(.+?)</a>%<a href="$2.html">$3</a> (<a href="$1">online</a>)%i; + # References to other bug lists on bugs.d.o/release-critical + if (m%<a href="((?:debian|other)[-a-z/]+\.html)"%i) { + my $ref = 'release-critical/' . $1; + $ref =~ s%/%_%g; +s%<a href="((?:debian|other)[-a-z/]+\.html)">(.+?)</a>%<a href="$ref">$2</a> (<a href="${btsurl}release-critical/$1">online</a>)%i; + } + # Maintainer email address - YUCK!! +s%<a href="(https?://$btsserver/([^\"?]*\@[^\"?]*))">(.+?)</a>>%<a href="$2.html">$3</a>> (<a href="$1">online</a>)%i; + # Graph - we don't download +s%<img src="graph.png" alt="Graph of RC bugs">%<img src="${btsurl}release-critical/graph.png" alt="Graph of RC bugs (online)">%; + } + } else { + # Yuck, yuck, yuck. This regex splits the $data string at every + # occurrence of either "[<a " or plain "<a ", preserving any "[". + @data = split /(?:(?=\[<[Aa]\s)|(?<!\[)(?=<[Aa]\s))/, $data; + foreach (@data) { + if ( +m%<a(?: class=\".*?\")? href=\"(?:/cgi(?:-bin)?/)?bugreport\.cgi[^\?]*\?.*?;?bug=(\d+)%i + ) { + my $bug = $1; + my ($msg, $filename) = href_to_filename($_); + if ($bug eq $thing and defined $msg) { + if ($fullmode + or (!$fullmode and exists $$bug2filename{$msg})) { +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(bugreport\.cgi[^\"]*)">(.+?)</a>%<a$1 href="$filename">$3</a> (<a$1 href="$btscgiurl$2">online</a>)%i; + } else { +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(bugreport\.cgi[^\"]*)">(.+?)</a>%$3 (<a$1 href="$btscgiurl$2">online</a>)%i; + } + } else { +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(bugreport\.cgi[^\?]*\?.*?bug=(\d+))"(.*?)>(.+?)</a>%<a$1 href="$3.html"$4>$5</a> (<a$1 href="$btscgiurl$2">online</a>)%i; + } + } else { +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(pkgreport\.cgi\?(?:pkg|maint)=([^\"&;]+)[^\"]*)">(.+?)</a>%<a$1 href="$3.html">$4</a> (<a$1 href="$btscgiurl$2">online</a>)%gi; +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(pkgreport\.cgi\?src=([^\"&;]+)[^\"]*)">(.+?)</a>%<a$1 href="src_$3.html">$4</a> (<a$1 href="$btscgiurl$2">online</a>)%i; +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(pkgreport\.cgi\?submitter=([^\"&;]+)[^\"]*)">(.+?)</a>%<a$1 href="from_$3.html">$4</a> (<a$1 href="$btscgiurl$2">online</a>)%i; +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(pkgreport\.cgi\?.*?;?archive=([^\"&;]+);submitter=([^\"&;]+)[^\"]*)">(.+?)</a>%<a$1 href="from_$4_3Barchive_3D$3.html">$5</a> (<a$1 href="$btscgiurl$2">online</a>)%i; +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(pkgreport\.cgi\?.*?;?package=([^\"&;]+)[^\"]*)">(.+?)</a>%<a$1 href="$3.html">$4</a> (<a$1 href="$btscgiurl$2">online</a>)%gi; +s%<a((?: class=\".*?\")?) href="(?:/cgi(?:-bin)?/)?(bugspam\.cgi[^\"]+)">%<a$1 href="$btscgiurl$2">%i; +s%<a((?: class=\".*?\")?) href="/([0-9]+?)">(.+?)</a>%<a$1 href="$2.html">$3</a> (<a$1 href="$btsurl$2">online</a>)%i; + + # Version graphs + # - remove 'package=' and move the package to the front +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?)([^\"]+)package=([^;\"]+)([^\"]+\"|\")>%$1$3;$2$4>%gi; + # - replace 'found=' with '.f.' and 'fixed=' with '.fx.' + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?)(.*?;)found=([^\"]+)\">%$1$2.f.$3">%i; + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?)(.*?;)fixed=([^\"]+)\">%$1$2.fx.$3">%i; + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?found=)([^\"]+)\">%$1.f.$2">%i; + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?fixed=)([^\"]+)\">%$1.fx.$2">%i; + # - replace '%2F' or '%2C' (a URL-encoded / or ,) with '.' + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?[^\%]*)\%2[FC]([^\"]+)\">%$1.$2">%gi; + # - display collapsed graph images at 25% +s%(<img[^>]* src=\"[^\"]+);collapse=1([^\"]+)\">%$1$2.co" width="25\%" height="25\%">%gi; + # - and link to the collapsed graph + s%(<a[^>]* href=\"[^\"]+);collapse=1([^\"]+)\">%$1$2.co">%gi; + # - remove any other parameters + 1 while +s%((?:<img[^>]* src|<a[^>]* href)=\"(?:/cgi(?:-bin)?/)?version\.cgi\?[^\"]+);(?:\w+=\d+)([^>]+)\>%$1$2>%gi; + # - remove any +s (encoded spaces) + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?[^\+]*)\+([^\"]+)\">%$1$2">%gi; + # - remove trailing ";" and ";." from previous substitutions + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?[^\"]+);\.(.*?)>%$1.$2>%gi; + 1 while +s%((?:<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?[^\"]+);\">%$1">%gi; + # - final reference should be $package.$versions[.co].png +s%(<img[^>]* src=\"|<a[^>]* href=\")(?:/cgi(?:-bin)?/)?version\.cgi\?([^\"]+)(\"[^>]*)>%$1$2.png$3>%gi; + } + } + } + + return join("", @data); +} + +# Removes a specified thing from the cache +sub deletecache { + my $thing = shift; + my $thgopts = shift || ''; + + if (!-d $cachedir) { + die "$progname: deletecache() called but no cachedir!\n"; + } + + delete_timestamp($thing, $thgopts); + unlink cachefile($thing, $thgopts); + if ($thing =~ /^\d+$/) { + rmtree("$cachedir/$thing", 0, 1) if -d "$cachedir/$thing"; + unlink("$cachedir/$thing.mbox") if -f "$cachedir/$thing.mbox"; + unlink("$cachedir/$thing.status.mbox") + if -f "$cachedir/$thing.status.mbox"; + unlink("$cachedir/$thing.raw.mbox") if -f "$cachedir/$thing.raw.mbox"; + } +} + +# Given a thing, returns the filename for it in the cache. +sub cachefile { + my $thing = shift; + my $thgopts = shift || ''; + if ($thing eq '') { die "$progname: cachefile given empty argument\n"; } + if ($thing =~ /bugs.css$/) { return $cachedir . "bugs.css" } + $thing =~ s/^src:/src_/; + $thing =~ s/^from:/from_/; + $thing =~ s/^tag:/tag_/; + $thing =~ s/^usertag:/usertag_/; + $thing =~ s%^release-critical/index\.html$%release-critical.html%; + $thing =~ s%/%_%g; + $thgopts =~ s/;/_3B/g; + $thgopts =~ s/=/_3D/g; + return File::Spec->catfile($cachedir, + $thing . $thgopts . ($thing =~ /\.html$/ ? "" : ".html")); +} + +# Given a thing, returns the filename for its mbox in the cache. +sub mboxfile { + my $thing = shift; + return $thing =~ /^\d+$/ + ? File::Spec->catfile($cachedir, $thing . ".mbox") + : undef; +} + +# Given a bug number, returns the dirname for it in the cache. +sub cachebugdir { + my $thing = shift; + if ($thing !~ /^\d+$/) { + die "$progname: cachebugdir given faulty argument: $thing\n"; + } + return File::Spec->catdir($cachedir, $thing); +} + +# And the reverse: Given a filename in the cache, returns the corresponding +# "thing". +sub cachefile_to_thing { + my $thing = basename(shift, '.html'); + my $thgopts = ''; + $thing =~ s/^src_/src:/; + $thing =~ s/^from_/from:/; + $thing =~ s/^tag_/tag:/; + $thing =~ s/^usertag_/usertag:/; + $thing =~ s%^release-critical\.html$%release-critical/index\.html%; + $thing =~ s%_%/%g; + $thing =~ s/_3B/;/g; + $thing =~ s/_3D/=/g; + $thing =~ /^(.*?)((?:;.*)?)$/; + ($thing, $thgopts) = ($1, $2); + return ($thing, $thgopts); +} + +# Given a thing, gives the official BTS cgi page for it +sub thing_to_url { + my $thing = shift; + my $thingurl; + + # have to be intelligent here :/ + if ($thing =~ /^\d+$/) { + $thingurl = $btscgibugurl . "?bug=" . $thing; + } elsif ($thing =~ /^from:/) { + ($thingurl = $thing) =~ s/^from:/submitter=/; + $thingurl = $btscgipkgurl . '?' . $thingurl; + } elsif ($thing =~ /^src:/) { + ($thingurl = $thing) =~ s/^src:/src=/; + $thingurl = $btscgipkgurl . '?' . $thingurl; + } elsif ($thing =~ /^tag:/) { + ($thingurl = $thing) =~ s/^tag:/tag=/; + $thingurl = $btscgipkgurl . '?' . $thingurl; + } elsif ($thing =~ /^usertag:/) { + ($thingurl = $thing) =~ s/^usertag:/tag=/; + $thingurl = $btscgipkgurl . '?' . $thingurl; + } elsif ($thing =~ m%^release-critical(\.html|/(index\.html)?)?$%) { + $thingurl = $btsurl . 'release-critical/index.html'; + } elsif ($thing =~ m%^release-critical/%) { + $thingurl = $btsurl . $thing; + } elsif ($thing =~ /\@/) { # so presume it's a maint request + $thingurl = $btscgipkgurl . '?maint=' . $thing; + } else { # it's a package, or had better be... + $thingurl = $btscgipkgurl . '?pkg=' . $thing; + } + + return $thingurl; +} + +# Given a thing, reads all links to bugs from the corresponding cache file +# if there is one, and returns a list of them. +sub bugs_from_thing { + my $thing = shift; + my $thgopts = shift || ''; + my $cachefile = cachefile($thing, $thgopts); + + if (-f $cachefile) { + local $/; + open(IN, $cachefile) || die "$progname: open $cachefile: $!\n"; + my $data = <IN>; + close IN; + + return $data =~ m!href="(\d+)\.html"!g; + } else { + return (); + } +} + +# Given an <a href="bugreport.cgi?...>...</a> string, return a +# msg id and corresponding filename +sub href_to_filename { + my $href = $_[0]; + my ($msg, $filename); + + if ($href + =~ m%\[<a(?: class=\".*?\")? href="((?:/cgi(?:-bin)?/)?bugreport\.cgi([^\?]*)\?[^\"]*)">.*?\(([^,]*), .*?\)\]% + ) { + # this looks like an attachment; $4 should give the MIME-type + my $uri = URI->new($1); + my $urlfilename = $2; + my $bug = $uri->query_param_delete('bug'); + my $mimetype = $3; + + my $ref = $uri->query(); + $ref =~ s/&(?:amp;)?/;/g; # normalise all hrefs + $uri->query($ref); + + $msg = $uri->query_param('msg'); + my $att = $uri->query_param('att'); + return undef unless $msg && $att; + $msg .= "-$att"; + $urlfilename ||= $att // ''; + + my $fileext = ''; + if ($urlfilename =~ m%^/%) { + $filename = basename($urlfilename); + } else { + $filename = ''; + if ($mimetype eq 'text/plain') { $fileext = '.txt'; } + if ($mimetype eq 'text/html') { $fileext = '.html'; } + } + if (length($filename)) { + $filename = "$bug/$msg/$filename"; + } else { + $filename = "$bug/$msg$fileext"; + } + } elsif ($href + =~ m%<a(?: class=\".*?\")? href="((?:/cgi(?:-bin)?/)?bugreport\.cgi([^\?]*)\?([^"]*))".*?>% + ) { + my $uri = URI->new($1); + my $urlfilename = $2; + my $bug = $uri->query_param_delete('bug'); + $msg = $uri->query_param_delete('msg'); + + my $ref = $uri->query // ''; + $ref =~ s/&(?:amp;)?/;/g; # normalise all hrefs + $ref =~ s/;archive=(yes|no)\b//; + $ref =~ s/%3D/=/g; + $uri->query($ref); + + my %params = ( + mboxstatus => '', + mboxstat => '', + mboxmaint => '', + mbox => '', + $uri->query_form(), + ); + + if ($msg && !%params) { + $filename = File::Spec->catfile($bug, "$msg.html"); + } elsif (($params{mboxstat} || $params{mboxstatus}) eq 'yes') { + $msg = 'statusmbox'; + $filename = "$bug.status.mbox"; + } elsif ($params{mboxmaint} eq 'yes') { + $msg = 'mbox'; + $filename = "$bug.mbox"; + } elsif ($params{mbox} eq 'yes') { + if ($msg) { + $filename = "$bug/$msg.mbox"; + $msg .= '-mbox'; + } else { + $filename = "$bug.raw.mbox"; + $msg = 'rawmbox'; + } + } elsif (!$ref) { + return undef; + } else { + $href =~ s/>.*/>/s; + warn +"$progname: in href_to_filename: unrecognised BTS URL type: $href\n"; + return undef; + } + } elsif ($href + =~ m%<(?:a[^>]* href|img [^>]* src)="((?:/cgi(?:-bin)?/)?version\.cgi\?[^"]+)"[^>]*>%i + ) { + my $uri = URI->new($1); + my %params = $uri->query_form(); + + if ($params{package}) { + $filename .= $params{package}; + } + if ($params{found}) { + $filename .= ".f.$params{found}"; + } + if ($params{fixed}) { + $filename .= ".fx.$params{fixed}"; + } + if ($params{collapse}) { + $filename .= '.co'; + } + + # Replace encoded "/" and "," characters with "." + $filename =~ s@(?:%2[FC]|/|,)@.@gi; + # Remove encoded spaces + $filename =~ s/\+//g; + + $msg = 'versions'; + $filename .= '.png'; + } else { + return undef; + } + + return ($msg, $filename); +} + +# Browses a given thing, with preprocessed list of URL options such as +# ";opt1=val1;opt2=val2" with possible caching if there are no options +sub browse { + prunecache(); + my $thing = shift; + my $thgopts = shift || ''; + + if ($thing eq '') { + if ($thgopts ne '') { + die +"$progname: you can only give options for a BTS page if you specify a bug/maint/... .\n"; + } + runbrowser($btsurl); + return; + } + + my $hascache = -d $cachedir; + my $cachefile = cachefile($thing, $thgopts); + my $mboxfile = mboxfile($thing); + if ($mboxmode and !$mboxfile) { + die +"$progname: you can only request a mailbox for a single bug report.\n"; + } + + # Check that if we're requesting a tag, that it's a valid tag + if (($thing . $thgopts) =~ /(?:^|;)(?:tag|include|exclude)[:=]([^;]*)/) { + unless (exists $valid_tags{$1}) { + die +"$progname: invalid tag requested: $1\nRecognised tag names are: " + . join(" ", @valid_tags) . "\n"; + } + } + + my $livedownload = 1; + if ($offlinemode) { + $livedownload = 0; + if (!$hascache) { + die +"$progname: Sorry, you are in offline mode and have no cache.\nRun \"bts cache\" or \"bts show\" to create one.\n"; + } elsif ((!$mboxmode and !-r $cachefile) + or ($mboxmode and !-r $mboxfile)) { + die +"$progname: Sorry, you are in offline mode and that is not cached.\nUse \"bts [--cache-mode=...] cache\" to update the cache.\n"; + } + if ($mboxmode) { + runmailreader($mboxfile); + } else { + runbrowser("file://$cachefile"); + } + } + # else we're in online mode + elsif ($caching && have_lwp() && $thing ne '') { + if (!$hascache) { + if (!-d dirname($cachedir)) { + unless (make_path(dirname($cachedir))) { + warn "$progname: couldn't mkdir " + . dirname($cachedir) + . ": $!\n"; + goto LIVE; + } + } + unless (make_path($cachedir)) { + warn "$progname: couldn't mkdir $cachedir: $!\n"; + goto LIVE; + } + } + + $livedownload = 0; + my $live = download($thing, $thgopts, 0, $mboxmode); + + if ($mboxmode) { + runmailreader($mboxfile); + } else { + if (length($live)) { + my ($fh, $livefile) = tempfile( + "btsXXXXXX", + SUFFIX => ".html", + DIR => File::Spec->tmpdir, + UNLINK => 1 + ); + + # Use filehandle for security + open(OUT_LIVE, ">&", $fh) + or die "$progname: writing to temporary file: $!\n"; + # Correct relative urls to point to the bts. + $live + =~ s%\shref="(?:/cgi(?:-bin)?/)?(\w+\.cgi)% href="$btscgiurl$1%g; + print OUT_LIVE $live; + # Some browsers don't like unseekable filehandles, + # so use filename + runbrowser("file://$livefile"); + } else { + runbrowser("file://$cachefile"); + } + } + } + + LIVE: # we are not caching; just show it live + if ($livedownload) { + if ($mboxmode) { + # we appear not to be caching; OK, we'll download to a + # temporary file + warn +"bts debug: downloading ${btscgiurl}bugreport.cgi?bug=$thing;mbox=yes\n" + if $debug; + my ($fh, $fn) = download_mbox($thing, 1); + runmailreader($fn); + } else { + if ($thgopts ne '') { + my $thingurl = thing_to_url($thing); + runbrowser($thingurl . $thgopts); + } else { + # let the BTS be intelligent + runbrowser($btsurl . $thing); + } + } + } +} + +# Removes all files from the cache which were downloaded automatically +# and have not been accessed for more than 30 days. We also only run +# this at most once per day for efficiency. + +sub prunecache { + # TODO: Remove handling of $oldcache post-Stretch + my $oldcache = File::Spec->catdir($ENV{HOME}, '.devscripts_cache', 'bts'); + if (-d $oldcache && !-d $cachedir) { + my $err; + make_path(dirname($cachedir), { error => \$err }); + if (!@$err) { + system('mv', $oldcache, $cachedir); + } + } + return unless -d $cachedir; + return if -f $prunestamp and -M _ < 1; + + my $oldcwd = getcwd; + + chdir($cachedir) || die "$progname: chdir $cachedir: $!\n"; + + # remove the now-defunct live-download file + unlink "live_download.html"; + + opendir DIR, '.' or die "$progname: opendir $cachedir: $!\n"; + my @cachefiles = grep { !/^\.\.?$/ } readdir(DIR); + closedir DIR; + + # Are there any unexpected files lying around? + my @known_files = map { basename($_) } + ($timestampdb, $timestampdb . ".lock", $prunestamp); + + my %weirdfiles = map { $_ => 1 } grep { !/\.(html|css|png)$/ } @cachefiles; + foreach (@known_files) { + delete $weirdfiles{$_} if exists $weirdfiles{$_}; + } + # and bug directories + foreach (@cachefiles) { + if (/^(\d+)\.html$/) { + delete $weirdfiles{$1} if exists $weirdfiles{$1} and -d $1; + delete $weirdfiles{"$1.mbox"} + if exists $weirdfiles{"$1.mbox"} and -f "$1.mbox"; + delete $weirdfiles{"$1.raw.mbox"} + if exists $weirdfiles{"$1.raw.mbox"} and -f "$1.raw.mbox"; + delete $weirdfiles{"$1.status.mbox"} + if exists $weirdfiles{"$1.status.mbox"} and -f "$1.status.mbox"; + } + } + + warn "$progname: unexpected files/dirs in cache directory $cachedir:\n " + . join("\n ", keys %weirdfiles) . "\n" + if keys %weirdfiles; + + my @oldfiles; + foreach (@cachefiles) { + next unless /\.(html|css)$/; + push @oldfiles, $_ if -A $_ > 30; + } + + # We now remove the oldfiles if they're automatically downloaded + tie(%timestamp, "Devscripts::DB_File_Lock", $timestampdb, + O_RDWR() | O_CREAT(), + 0600, $DB_HASH, "write") + or die "$progname: couldn't open DB file $timestampdb for writing: $!\n" + if !tied %timestamp; + + my @unrecognised; + foreach my $oldfile (@oldfiles) { + my ($thing, $thgopts) = cachefile_to_thing($oldfile); + unless (defined get_timestamp($thing, $thgopts)) { + push @unrecognised, $oldfile; + next; + } + next if is_manual(get_timestamp($thing, $thgopts)); + + # Otherwise, it's automatic and we purge it + deletecache($thing, $thgopts); + } + + untie %timestamp; + + if (!-e $prunestamp) { + open PRUNESTAMP, + ">$prunestamp" || die "$progname: prune timestamp: $!\n"; + close PRUNESTAMP; + } + chdir $oldcwd || die "$progname: chdir $oldcwd: $!\n"; + utime time, time, $prunestamp; +} + +# Determines which browser to use +sub runbrowser { + my $URL = shift; + + if (system('sensible-browser', $URL) >> 8 != 0) { + warn "Problem running sensible-browser: $!\n"; + } +} + +# Determines which mailreader to use +sub runmailreader { + my $file = shift; + my $quotedfile; + die "$progname: could not read mbox file $file!\n" unless -r $file; + + if ($file !~ /\'/) { $quotedfile = qq['$file']; } + elsif ($file !~ /[\"\\\$\'\!]/) { $quotedfile = qq["$file"]; } + else { + die +"$progname: could not figure out how to quote the mbox filename \"$file\"\n"; + } + + my $reader = $mailreader; + $reader =~ s/\%([%s])/$1 eq '%' ? '%' : $quotedfile/eg; + + if (system($reader) >> 8 != 0) { + warn "Problem running mail reader: $!\n"; + } +} + +# Timestamp handling +# +# We store a +ve timestamp to represent an automatic download and +# a -ve one to represent a manual download. + +sub get_timestamp { + my $thing = shift; + my $thgopts = shift || ''; + my $timestamp = undef; + my $versionstamp = undef; + + if (tied %timestamp) { + ($timestamp, $versionstamp) = split /;/, + $timestamp{ $thing . $thgopts } + if exists $timestamp{ $thing . $thgopts }; + } else { + tie(%timestamp, "Devscripts::DB_File_Lock", $timestampdb, + O_RDONLY(), 0600, $DB_HASH, "read") + or die + "$progname: couldn't open DB file $timestampdb for reading: $!\n"; + + ($timestamp, $versionstamp) = split /;/, + $timestamp{ $thing . $thgopts } + if exists $timestamp{ $thing . $thgopts }; + + untie %timestamp; + } + + return wantarray ? ($timestamp, $versionstamp) : $timestamp; +} + +sub set_timestamp { + my $thing = shift; + my $thgopts = shift || ''; + my $timestamp = shift; + my $versionstamp = shift || $version; + + if (tied %timestamp) { + $timestamp{ $thing . $thgopts } = "$timestamp;$versionstamp"; + } else { + tie(%timestamp, "Devscripts::DB_File_Lock", $timestampdb, + O_RDWR() | O_CREAT(), + 0600, $DB_HASH, "write") + or die + "$progname: couldn't open DB file $timestampdb for writing: $!\n"; + + $timestamp{ $thing . $thgopts } = "$timestamp;$versionstamp"; + + untie %timestamp; + } +} + +sub delete_timestamp { + my $thing = shift; + my $thgopts = shift || ''; + + if (tied %timestamp) { + delete $timestamp{ $thing . $thgopts }; + } else { + tie(%timestamp, "Devscripts::DB_File_Lock", $timestampdb, + O_RDWR() | O_CREAT(), + 0600, $DB_HASH, "write") + or die + "$progname: couldn't open DB file $timestampdb for writing: $!\n"; + + delete $timestamp{ $thing . $thgopts }; + + untie %timestamp; + } +} + +sub is_manual { + return $_[0] < 0; +} + +sub make_manual { + return -abs($_[0]); +} + +sub is_automatic { + return $_[0] > 0; +} + +sub make_automatic { + return abs($_[0]); +} + +# Returns true if current cached version is older than critical version +# We're only using really simple version numbers here: a.b.c +sub old_cache_format_version { + my $cacheversion = $_[0]; + + my @cache = split /\./, $cacheversion; + my @new = split /\./, $new_cache_format_version; + + push @cache, 0, 0, 0, 0; + push @new, 0, 0; + + return + ($cache[0] < $new[0]) + || ($cache[0] == $new[0] && $cache[1] < $new[1]) + || ($cache[0] == $new[0] && $cache[1] == $new[1] && $cache[2] < $new[2]) + || ( $cache[0] == $new[0] + && $cache[1] == $new[1] + && $cache[2] == $new[2] + && $cache[3] < $new[3]); +} + +# We would love to use LWP::Simple::mirror in this script. +# Unfortunately, bugs.debian.org does not respect the +# If-Modified-Since header. For single bug reports, however, +# bugreport.cgi will return a Last-Modified header if sent a HEAD +# request. So this is a hack, based on code from the LWP modules. :-( +# Return value: +# (return value, error string) +# with return values: MIRROR_ERROR failed +# MIRROR_DOWNLOADED downloaded new version +# MIRROR_UP_TO_DATE up-to-date + +sub bts_mirror { + my ($url, $timestamp, $force) = @_; + + init_agent() unless $ua; + if ($url =~ m%/\d+$% and !$refreshmode and !$force) { + # Single bug, worth doing timestamp checks + my $request = HTTP::Request->new('HEAD', $url); + my $response = $ua->request($request); + + if ($response->is_success) { + my $lm = $response->last_modified; + if (defined $lm and $lm <= abs($timestamp)) { + return (MIRROR_UP_TO_DATE, $response->status_line); + } + } else { + return (MIRROR_ERROR, $response->status_line); + } + } + + # So now we download the full thing regardless + # We don't care if we scotch the contents of $file - it's only + # a temporary file anyway + my $request = HTTP::Request->new('GET', $url); + my $response = $ua->request($request); + + if ($response->is_success) { + # This check from LWP::UserAgent; I don't even know whether + # the BTS sends a Content-Length header... + my $nominal_content_length = $response->content_length || 0; + my $true_content_length + = defined $response->content ? length($response->content) : 0; + if ($true_content_length == 0) { + return (MIRROR_ERROR, $response->status_line); + } + if ($nominal_content_length > 0) { + if ($true_content_length < $nominal_content_length) { + return (MIRROR_ERROR, +"Transfer truncated: only $true_content_length out of $nominal_content_length bytes received" + ); + } + if ($true_content_length > $nominal_content_length) { + return (MIRROR_ERROR, +"Content-length mismatch: expected $nominal_content_length bytes, got $true_content_length" + ); + } + # else OK + } + + return ( + MIRROR_DOWNLOADED, $response->status_line, + $response->content, $response->header('Content-Type')); + } else { + return (MIRROR_ERROR, $response->status_line); + } +} + +sub init_agent { + $ua = new LWP::UserAgent; # we create a global UserAgent object + $ua->agent("LWP::UserAgent/Devscripts/$version"); + $ua->env_proxy; +} + +sub opts_done { + if (@_) { + die "$progname: unknown options to '$command[$index]': @_\n"; + } +} + +sub edit { + my $message = shift; + my ($fh, $filename); + ($fh, $filename) = tempfile( + "btsXXXX", + SUFFIX => ".mail", + DIR => File::Spec->tmpdir + ); + open(OUT_MAIL, ">$filename") + or die "$progname: writing to temporary file: $!\n"; + print OUT_MAIL $message; + close OUT_MAIL; + my $rc = system("sensible-editor $filename"); + undef $message; + + if ($rc == 0) { + open(OUT_MAIL, "<$filename") + or die "$progname: reading from temporary file: $!\n"; + while (<OUT_MAIL>) { + $message .= $_; + } + close OUT_MAIL; + } + unlink($filename); + return $message; +} + +=back + +=head1 ENVIRONMENT VARIABLES + +=over 4 + +=item B<DEBEMAIL> + +If this is set, the From: line in the email will be set to use this email +address instead of your normal email address (as would be determined by +B<mail>). + +=item B<DEBFULLNAME> + +If B<DEBEMAIL> is set, B<DEBFULLNAME> is examined to determine the full name +to use; if this is not set, B<bts> attempts to determine a name from +your F<passwd> entry. + +=item B<BROWSER> + +If set, it specifies the browser to use for the B<show> and B<bugs> +options. See the description above. + +=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<BTS_OFFLINE> + +If this is set to B<yes>, then it is the same as the B<--offline> command +line parameter being used. Only has an effect on the B<show> and B<bugs> +commands. The default is B<no>. See the description of the B<show> +command above for more information. + +=item B<BTS_CACHE> + +If this is set to B<no>, then it is the same as the B<--no-cache> command +line parameter being used. Only has an effect on the B<show> and B<bug> +commands. The default is B<yes>. Again, see the B<show> command above +for more information. + +=item B<BTS_CACHE_MODE=>{B<min>,B<mbox>,B<full>} + +How much of the BTS should we mirror when we are asked to cache something? +Just the minimum, or also the mbox or the whole thing? The default is +B<min>, and it has the same meaning as the B<--cache-mode> command line +parameter. Only has an effect on the cache. See the B<cache> command for more +information. + +=item B<BTS_FORCE_REFRESH> + +If this is set to B<yes>, then it is the same as the B<--force-refresh> +command line parameter being used. Only has an effect on the B<cache> +command. The default is B<no>. See the B<cache> command for more +information. + +=item B<BTS_MAIL_READER> + +If this is set, specifies a mail reader to use instead of B<mutt>. Same as +the B<--mailreader> command line option. + +=item B<BTS_SENDMAIL_COMMAND> + +If this is set, specifies a B<sendmail> command to use instead of +F</usr/sbin/sendmail>. Same as the B<--sendmail> command line option. + +=item B<BTS_ONLY_NEW> + +Download only new bugs when caching. Do not check for updates in +bugs we already have. The default is B<no>. Same as the B<--only-new> +command line option. + +=item B<BTS_SMTP_HOST> + +If this is set, specifies an SMTP host to use for sending mail rather +than using the B<sendmail> command. Same as the B<--smtp-host> command line +option. + +Note that this option takes priority over B<BTS_SENDMAIL_COMMAND> if both are +set, unless the B<--sendmail> option is used. + +=item B<BTS_SMTP_AUTH_USERNAME>, B<BTS_SMTP_AUTH_PASSWORD> + +If these options are set, then it is the same as the B<--smtp-username> and +B<--smtp-password> options being used. + +=item B<BTS_SMTP_HELO> + +Same as the B<--smtp-helo> command line option. + +=item B<BTS_INCLUDE_RESOLVED> + +If this is set to B<no>, then it is the same as the B<--no-include-resolved> +command line parameter being used. Only has an effect on the B<cache> +command. The default is B<yes>. See the B<cache> command for more +information. + +=item B<BTS_SUPPRESS_ACKS> + +If this is set to B<yes>, then it is the same as the B<--no-ack> command +line parameter being used. The default is B<no>. + +=item B<BTS_INTERACTIVE> + +If this is set to B<yes> or B<force>, then it is the same as the +B<--interactive> or B<--force-interactive> command line parameter being used. +The default is B<no>. + +=item B<BTS_DEFAULT_CC> + +Specify a list of e-mail addresses to which a carbon copy of the generated +e-mail to the control bot should automatically be sent. + +=item B<BTS_SERVER> + +Specify the name of a debbugs server which should be used instead of +https://bugs.debian.org. + +=back + +=head1 SEE ALSO + +Please see L<https://www.debian.org/Bugs/server-control> for +more details on how to control the BTS using emails and +L<https://www.debian.org/Bugs/> for more information about the BTS. + +querybts(1), reportbug(1), pts-subscribe(1), devscripts.conf(5) + +=head1 COPYRIGHT + +This program is Copyright (C) 2001-2003 by Joey Hess <joeyh@debian.org>. +Many modifications have been made, Copyright (C) 2002-2005 Julian +Gilbey <jdg@debian.org> and Copyright (C) 2007 Josh Triplett +<josh@freedesktop.org>. + +It is licensed under the terms of the GPL, either version 2 of the +License, or (at your option) any later version. + +=cut + +# Please leave this alone unless you understand the seek above. +__DATA__ diff --git a/scripts/build-rdeps.pl b/scripts/build-rdeps.pl new file mode 100755 index 0000000..c741371 --- /dev/null +++ b/scripts/build-rdeps.pl @@ -0,0 +1,558 @@ +#!/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> + +=head1 DESCRIPTION + +B<build-rdeps> searches for all packages that build-depend on the specified package. + +The default behaviour is to just `grep` for the given dependency 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 that package +installed, 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.) + +=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<--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 Dpkg::Control; +use Dpkg::Vendor qw(get_current_vendor); + +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 = 'Debian'; +my @opt_exclude_components; +my $opt_buildarch; +my $opt_hostarch; +my $opt_without_ceve; +my $opt_quiet; +my $opt_noarchall; +my $opt_noarchany; + +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 + --old Use the old simple reverse dependency resolution + +EOT + version; +} + +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) { + print STDERR "DEBUG: dose-ceve cannot be executed: $!\n" + if ($opt_debug); + $ceve_compatible = 0; + } elsif ($? == 0) { + $ceve_compatible = 1; + } else { + print STDERR "DEBUG: dose-ceve is too old\n" if ($opt_debug); + $ceve_compatible = 0; + } + return $ceve_compatible; +} + +sub is_devel_release { + my $ctrl = shift; + if (get_current_vendor() 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'); + } + + print STDERR 'DEBUG: Running ' . join(' ', map { "'$_'" } @cmd) . "\n" + if $opt_debug; + 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; + } + # Only need Sources/Packages stanzas + if ( $ctrl->{'Created-By'} ne 'Packages' + && $ctrl->{'Created-By'} ne 'Sources') { + 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/) { + next; + } + } + + # And the provided distribution + if ($opt_distribution) { + if ( $ctrl->{Suite} !~ m/\Q$opt_distribution\E/ + && $ctrl->{Codename} !~ m/\Q$opt_distribution\E/) { + next; + } + } elsif ($opt_develonly && !is_devel_release($ctrl)) { + 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}; + print STDERR "DEBUG: Added source file: $ctrl->{Filename}\n" + if $opt_debug; + } else { + $ref->{ $ctrl->{Architecture} } = $ctrl->{Filename}; + } + } + close($targets); + + return \%info; +} + +sub findreversebuilddeps { + my ($package, $info) = @_; + my $count = 0; + + my $source_file = $info->{sources}; + if ($use_ceve) { + die "build arch undefined" if !defined $opt_buildarch; + die "host arch undefined" if !defined $opt_hostarch; + + my $buildarch_file = $info->{$opt_buildarch}; + my $hostarch_file = $info->{$opt_hostarch}; + + my @ceve_cmd = ( + 'dose-ceve', '-T', + 'debsrc', '-r', + $package, '-G', + 'pkg', "--deb-native-arch=$opt_buildarch", + "deb://$buildarch_file", "debsrc://$source_file" + ); + if ($opt_buildarch ne $opt_hostarch) { + push(@ceve_cmd, + "--deb-host-arch=$opt_hostarch", + "deb://$hostarch_file"); + } + push(@ceve_cmd, "--deb-drop-b-d-indep") if ($opt_noarchall); + push(@ceve_cmd, "--deb-drop-b-d-arch") if ($opt_noarchany); + my %sources; + print STDERR 'DEBUG: executing: ' . join(' ', @ceve_cmd) + if ($opt_debug); + open(SOURCES, '-|', @ceve_cmd); + while (<SOURCES>) { + next unless s/^Package:\s+//; + chomp; + $sources{$_} = 1; + } + for my $source (sort keys %sources) { + 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 %packages; + 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); + 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,]|$)/ + ) { + $packages{ $ctrl->{Package} }{Maintainer} + = $ctrl->{Maintainer}; + } + } + } + } + + close($out); + + while (my $depending_package = each(%packages)) { + print $depending_package; + if ($opt_maintainer) { + print " ($packages{$depending_package}->{'Maintainer'})"; + } + print "\n"; + $count += 1; + } + } + + if (!$opt_quiet) { + if ($count == 0) { + print "No reverse build-depends found for $package.\n\n"; + } else { + print +"\nFound a total of $count reverse build-depend(s) for $package.\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, + # "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 $package = shift; + +if (!$package) { + die "$progname: missing argument. expecting packagename\n"; +} + +print STDERR "DEBUG: Package => $package\n" if ($opt_debug); + +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 (system('command -v grep-dctrl >/dev/null 2>&1')) { + die +"$progname: Fatal error. grep-dctrl is not available.\nPlease install the 'dctrl-tools' package.\n"; + } + + # 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; + } + print STDERR "DEBUG: running with dose-ceve resolver\n" if ($opt_debug); + print STDERR "DEBUG: buildarch=$opt_buildarch hostarch=$opt_hostarch\n" + if ($opt_debug); +} else { + print STDERR "DEBUG: running with old resolver\n" if ($opt_debug); +} + +if ($opt_update) { + print STDERR "DEBUG: Updating apt-cache before search\n" if ($opt_debug); + my @cmd; + if ($opt_sudo) { + print STDERR "DEBUG: Using sudo to become root\n" if ($opt_debug); + 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)) { + if (exists $file_info->{$site}{$suite}{$comp}) { + if (!$opt_quiet) { + my $msg = "Reverse Build-depends in $suite/$comp:"; + print "$msg\n"; + print "-" x length($msg) . "\n\n"; + } + findreversebuilddeps($package, + $file_info->{$site}{$suite}{$comp}); + } + } + } +} + +=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 diff --git a/scripts/chdist.bash_completion b/scripts/chdist.bash_completion new file mode 100644 index 0000000..51dbf49 --- /dev/null +++ b/scripts/chdist.bash_completion @@ -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 : diff --git a/scripts/chdist.pl b/scripts/chdist.pl new file mode 100755 index 0000000..b473b95 --- /dev/null +++ b/scripts/chdist.pl @@ -0,0 +1,778 @@ +#!/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>] + +=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<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; +no if $] >= 5.018, 'warnings', 'experimental::smartmatch'; +use feature 'switch'; +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 + 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; +given ($command) { + when ('create') { + dist_create(@ARGV); + } + when ('apt') { + aptcmd('apt', @ARGV); + } + when ('apt-get') { + aptcmd('apt-get', @ARGV); + } + when ('apt-cache') { + aptcmd('apt-cache', @ARGV); + } + when ('apt-file') { + apt_file(@ARGV); + } + when ('apt-rdepends') { + aptcmd('apt-rdepends', @ARGV); + } + when ('aptitude') { + aptcmd('aptitude', @ARGV); + } + when ('bin2src') { + bin2src(@ARGV); + } + when ('src2bin') { + src2bin(@ARGV); + } + when ('compare-packages') { + dist_compare(@ARGV, 0, 'Sources'); + } + when ('compare-bin-packages') { + dist_compare(@ARGV, 0, 'Packages'); + } + when ('compare-versions') { + dist_compare(@ARGV, 1, 'Sources'); + } + when ('compare-bin-versions') { + dist_compare(@ARGV, 1, 'Packages'); + } + when ('grep-dctrl-packages') { + grep_file(@ARGV, 'Packages'); + } + when ('grep-dctrl-sources') { + grep_file(@ARGV, 'Sources'); + } + when ('compare-src-bin-packages') { + compare_src_bin(@ARGV, 0); + } + when ('compare-src-bin-versions') { + compare_src_bin(@ARGV, 1); + } + when ('list') { + list; + } + default { + my $dist = $command; + my $dir = "$datadir/$dist"; + if (-d $dir && !$recursed) { + splice @ARGV, 1, 0, $dist; + $recursed = 1; + goto MAIN; + } elsif ($dist && !$recursed) { + dist_check($dist); + } else { + usage(1); + } + } +} diff --git a/scripts/checkbashisms.1 b/scripts/checkbashisms.1 new file mode 100644 index 0000000..8f706ab --- /dev/null +++ b/scripts/checkbashisms.1 @@ -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>. diff --git a/scripts/checkbashisms.bash_completion b/scripts/checkbashisms.bash_completion new file mode 100644 index 0000000..b0e30fd --- /dev/null +++ b/scripts/checkbashisms.bash_completion @@ -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 : diff --git a/scripts/checkbashisms.pl b/scripts/checkbashisms.pl new file mode 100755 index 0000000..b775e51 --- /dev/null +++ b/scripts/checkbashisms.pl @@ -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>; + } +} diff --git a/scripts/cowpoke.1 b/scripts/cowpoke.1 new file mode 100644 index 0000000..7d5177b --- /dev/null +++ b/scripts/cowpoke.1 @@ -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 gpg 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>. + diff --git a/scripts/cowpoke.sh b/scripts/cowpoke.sh new file mode 100755 index 0000000..54dd9fc --- /dev/null +++ b/scripts/cowpoke.sh @@ -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 diff --git a/scripts/cvs-debc.1 b/scripts/cvs-debc.1 new file mode 100644 index 0000000..98a399e --- /dev/null +++ b/scripts/cvs-debc.1 @@ -0,0 +1,67 @@ +.TH CVS-DEBC 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +cvs-debc \- view contents of a cvs-buildpackage/cvs-debuild generated package +.SH SYNOPSIS +\fBcvs-debc\fP [\fIoptions\fR] [\fIpackage\fR ...] +.SH DESCRIPTION +\fBcvs-debc\fR is run from the CVS working directory after +\fBcvs-buildpackage\fR or \fBcvs-debuild\fR. It uses the +\fBcvs-buildpackage\fR system to locate the \fI.changes\fR file +generated in that run. It then displays information about the \fI.deb\fR +files which were generated in that run, by running \fBdpkg-deb \-I\fR +and \fBdpkg-deb \-c\fR on every \fI.deb\fR archive listed in +the \fI.changes\fR file, assuming that all of the \fI.deb\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 with names in this list of packages will be processed. +.PP +Note that unlike \fBcvs-buildpackage\fR, the only way to specify the +source package name is with the \fB\-P\fR option; you cannot simply +have it as the last command-line parameter. +.SH OPTIONS +All current \fBcvs-buildpackage\fR options are silently accepted; +however, only the ones listed below have any effect. For more details +on all of them, see the \fBcvs-buildpackage\fR(1) manpage. +.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 +.BR \-M\fImodule +The name of the CVS module. +.TP +.BR \-P\fIpackage +The name of the package. +.TP +.B \-V\fIversion +The version number of the package. +.TP +.B \-T\fItag +The CVS tag to use for exporting sources. +.TP +.B \-R\fIroot\ directory +Root of the original sources archive. +.TP +.B \-W\fIwork directory +The full path name for the cvs-buildpackage working directory. +.TP +.B \-x\fIprefix +This option provides the CVS default module prefix. +.TP +\fB\-\-help\fR, \fB\-\-version\fR +Show help message and version information respectively. +.SH "SEE ALSO" +.BR cvs-buildpackage (1), +.BR cvs-debi (1), +.BR cvs-debuild (1), +.BR debc (1) +.SH AUTHOR +\fBcvs-buildpackage\fR was written by Manoj Srivastava, and the +current version of \fBdebi\fR was written by Julian Gilbey +<jdg@debian.org>. They have been combined into this program by +Julian Gilbey. diff --git a/scripts/cvs-debi.1 b/scripts/cvs-debi.1 new file mode 100644 index 0000000..bb0ac8c --- /dev/null +++ b/scripts/cvs-debi.1 @@ -0,0 +1,71 @@ +.TH CVS-DEBI 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +cvs-debi \- install cvs-buildpackage/cvs-debuild generated package +.SH SYNOPSIS +\fBcvs-debi\fP [\fIoptions\fR] [\fIpackage\fR ...] +.SH DESCRIPTION +\fBcvs-debi\fR is run from the CVS working directory after +\fBcvs-buildpackage\fR or \fBcvs-debuild\fR. It uses the +\fBcvs-buildpackage\fR system to locate the \fI.changes\fR file +generated in that run. It then runs \fBdebpkg \-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 +Note that unlike \fBcvs-buildpackage\fR, the only way to specify the +source package name is with the \fB\-P\fR option; you cannot simply +have it as the last command-line parameter. +.PP +Since installing a package requires root privileges, \fBdebi\fR calls +\fBdebpkg\fR rather than \fBdpkg\fR directly. Thus \fBdebi\fR will +only be useful if it is either being run as root or \fBdebpkg\fR can +be run as root. See \fBdebpkg\fR(1) for more details. +.SH OPTIONS +All current \fBcvs-buildpackage\fR options are silently accepted; +however, only the ones listed below have any effect. For more details +on all of them, see the \fBcvs-buildpackage\fR(1) manpage. +.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 +.BR \-M\fImodule +The name of the CVS module. +.TP +.BR \-P\fIpackage +The name of the package. +.TP +.B \-V\fIversion +The version number of the package. +.TP +.B \-T\fItag +The CVS tag to use for exporting sources. +.TP +.B \-R\fIroot\ directory +Root of the original sources archive. +.TP +.B \-W\fIwork directory +The full path name for the cvs-buildpackage working directory. +.TP +.B \-x\fIprefix +This option provides the CVS default module prefix. +.TP +\fB\-\-help\fR, \fB\-\-version\fR +Show help message and version information respectively. +.SH "SEE ALSO" +.BR cvs-buildpackage (1), +.BR cvs-debc (1), +.BR cvs-debuild (1), +.BR debi (1) +.SH AUTHOR +\fBcvs-buildpackage\fR was written by Manoj Srivastava, and the +current version of \fBdebi\fR was written by Julian Gilbey +<jdg@debian.org>. They have been combined into this program by +Julian Gilbey. diff --git a/scripts/cvs-debi.sh b/scripts/cvs-debi.sh new file mode 100755 index 0000000..49b89d9 --- /dev/null +++ b/scripts/cvs-debi.sh @@ -0,0 +1,370 @@ +#!/bin/bash + +# cvs-debi: Install current version of deb package +# cvs-debc: List contents of current version of deb package +# +# Based on debi/debc; see them for copyright information +# Based on cvs-buildpackage, copyright 1997 Manoj Srivastava +# (CVS Id: cvs-buildpackage,v 1.58 2003/08/22 17:24:29 srivasta Exp) +# This code is copyright 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/>. + +set -e + +PROGNAME=${0##*/} PROGNAME="${PROGNAME%.sh}" # .sh for debugging purposes + +usage() { + if [ "$PROGNAME" = cvs-debi ]; then usage_i + elif [ "$PROGNAME" = cvs-debc ]; then usage_c + else echo "Unrecognised invocation name: $PROGNAME" >&2; exit 1 + fi; +} + +usage_i() { + echo \ +"Usage: $PROGNAME [options] [package ...] + Install the .deb file(s) just created by cvs-buildpackage or cvs-debuild, + as listed in the .changes file generated on that run. If packages are + listed, only install those specified binary packages from the .changes file. + + Note that unlike cvs-buildpackage, the only way to specify the + source package name is with the -P option; you cannot simply have it + as the last parameter. + + Also uses the cvs-buildpackage configuration files to determine the + location of the build tree, as described in the manpage. + + Available options: + -M<module> CVS module name + -P<package> Package name + -V<version> Package version + -T<tag> CVS tag to use + -R<root dir> Root directory + -W<work dir> Working directory + -x<prefix> CVS default module prefix + -a<arch> Search for .changes file made for Debian build <arch> + -t<target> Search for .changes file made for GNU <target> arch + --help Show this message + --version Show version and copyright information + Other cvs-buildpackage options will be silently ignored." +} + +usage_c() { + echo \ +"Usage: $PROGNAME [options] [package ...] + Display the contents of the .deb file(s) just created by + cvs-buildpackage or cvs-debuild, as listed in the .changes file generated + on that run. If packages are listed, only display those specified binary + packages from the .changes file. + + Note that unlike cvs-buildpackage, the only way to specify the + source package name is with the -P option; you cannot simply have it + as the last parameter. + + Also uses the cvs-buildpackage configuration files to determine the + location of the build tree, as described in its manpage. + + Available options: + -M<module> CVS module name + -P<package> Package name + -V<version> Package version + -T<tag> CVS tag to use + -R<root dir> Root directory + -W<work dir> Working directory + -x<prefix> CVS default module prefix + -a<arch> Search for .changes file made for Debian build <arch> + -t<target> Search for .changes file made for GNU <target> arch + --help Show this message + --version Show version and copyright information + Other cvs-buildpackage options will be silently ignored." +} + +version() { echo \ +"This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This code is copyright 2003, Julian Gilbey <jdg@debian.org>, +all rights reserved. +Based on original code by Christoph Lameter and Manoj Srivastava. +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." +} + +setq() { + # Variable Value Doc string + if [ "x$2" = "x" ]; then + echo >&2 "$PROGNAME: Unable to determine $3" + exit 1; + else + if [ ! "x$Verbose" = "x" ]; then + echo "$PROGNAME: $3 is $2"; + fi + eval "$1=\"\$2\""; + fi +} + +# Is cvs-buildpackage installed? +if ! command -v cvs-buildpackage > /dev/null; then + echo "$PROGNAME: need the cvs-buildpackage package installed to run this" >&2 + exit 1 +fi + +# Long term variables, which may be set in the cvsdeb config file or the +# environment: +# rootdir workdir (if all original sources are kept in one dir) + +TEMPDIR=$(mktemp -dt cvs-debi.XXXXXXXX) || { + echo "$PROGNAME: unable to create temporary directory" >&2 + echo "Aborting..." >&2 + exit 1 +} +TEMPFILE=$TEMPDIR/cl-tmp +trap 'rm -f "$TEMPFILE"; rmdir "$TEMPDIR"' EXIT + +TAGOPT= + +# Command line; will bomb out if unrecognised options +TEMP=$(getopt -a -s bash \ + -o hC:EH:G:M:P:R:T:U:V:W:Ff:dcnr:x:Bp:Dk:a:Sv:m:e:i:I:t: \ + --long help,version,ctp,tC,sgpg,spgp,us,uc,op \ + --long si,sa,sd,ap,sp,su,sk,sr,sA,sP,sU,sK,sR,ss,sn \ + -n "$PROGNAME" -- "$@") +eval set -- $TEMP + +while true ; do + case "$1" in + -h|--help) usage; exit 0 ; shift ;; + --version) version; exit 0 ; shift ;; + -M) opt_cvsmodule="$2" ; shift 2 ;; + -P) opt_package="$2" ; shift 2 ;; + -R) opt_rootdir="$2" ; shift 2 ;; + -T) opt_tag="$2" ; shift 2 ;; + -V) opt_version="$2" ; shift 2 ;; + -W) opt_workdir="$2" ; shift 2 ;; + -x) opt_prefix="$2" ; shift 2 ;; + -a) targetarch="$2" ; shift 2 ;; + -t) if [ "$2" != "C" ]; then targetgnusystem="$2"; fi + shift 2 ;; + + # everything else is silently ignored + -[CHfGUr]) shift 2 ;; + -[FnE]) shift ;; + --ctp|--op|--tC) shift ;; + -[dDBbS]) shift ;; + -p) shift 2 ;; + --us|--uc|--sgpg|--spgp) shift ;; + --s[idapukrAPUKRns]) shift ;; + --ap) shift ;; + -[kvmeiI]) shift 2 ;; + + --) shift ; break ;; + *) echo >&2 "Internal error! ($1)" + usage; exit 1 ;; + esac +done + +if [ "x$opt_cvsmodule" = "x" -a "x$opt_package" = "x" -a \ + ! -e 'debian/changelog' ] ; then + echo >&2 "$PROGNAME should be run in the top working directory of" + echo >&2 "a Debian Package, or an explicit package (or CVS module) name" + echo >&2 "should be given." + exit 1 +fi + +if [ "x$opt_tag" != "x" ]; then + TAGOPT=-r$opt_tag +fi + +# Command line, env variable, config file, or default +# This anomalous position is in case we need to check out the changelog +# below (anomalous since we have not loaded the config file yet) +if [ ! "x$opt_prefix" = "x" ]; then + prefix="$opt_prefix" +elif [ ! "x$CVSDEB_PREFIX" = "x" ]; then + prefix="$CVSDEB_PREFIX" +elif [ ! "x$conf_prefix" = "x" ]; then + prefix="$conf_prefix" +else + prefix="" +fi + +# put a slash at the end of the prefix +if [ "X$prefix" != "X" ]; then + prefix="$prefix/"; + prefix=$(echo $prefix | sed 's://:/:g'); +fi + +if [ ! -f CVS/Root ]; then + if [ "X$CVSROOT" = "X" ]; then + echo "no CVS/Root file found, and CVSROOT var is empty" >&2 + exit 1 + fi +else + CVSROOT=$(cat CVS/Root) + export CVSROOT +fi + +if [ "x$opt_package" = "x" ]; then + # Get the official package name and version. + if [ -f debian/changelog ]; then + # Ok, changelog exists + setq "package" \ + "`dpkg-parsechangelog -SSource`" \ + "source package" + setq "version" \ + "`dpkg-parsechangelog -SVersion`" \ + "source version" + elif [ "x$opt_cvsmodule" != "x" ]; then + # Hmm. Well, see if we can checkout the changelog file + rm -f $TEMPFILE + cvs -q co -p $TAGOPT $opt_cvsmodule/debian/changelog > $TEMPFILE + setq "package" \ + "`dpkg-parsechangelog -l$TEMPFILE -SSource`" \ + "source package" + setq "version" \ + "`dpkg-parsechangelog -l$TEMPFILE -SVersion`" \ + "source version" + rm -f "$TEMPFILE" + else + # Well. We don't know what this package is. + echo >&2 " This does not appear be a Debian source tree, since" + echo >&2 " there is no debian/changelog, and there was no" + echo >&2 " package name or cvs module given on the command line" + echo >&2 " it is hard to figure out what the package name " + echo >&2 " should be. I give up." + exit 1 + fi +else + # The user knows best; package name is provided + setq "package" "$opt_package" "source package" + + # Now, the version number + if [ "x$opt_version" != "x" ]; then + # All hail the user provided value + setq "version" "$opt_version" "source package" + elif [ -f debian/changelog ]; then + # Fine, see what the changelog says + setq "version" \ + "`dpkg-parsechangelog -SVersion`" \ + "source version" + elif [ "x$opt_cvsmodule" != "x" ]; then + # Hmm. The CVS module name is known, so lets us try exporting changelog + rm -f $TEMPFILE + cvs -q co -p $TAGOPT $opt_cvsmodule/debian/changelog > $TEMPFILE + setq "version" \ + "`dpkg-parsechangelog -l$TEMPFILE -SVersion`" \ + "source version" + rm -f "$TEMPFILE" + else + # Ok, try exporting the package name + rm -f $TEMPFILE + cvsmodule="${prefix}$package" + cvs -q co -p $TAGOPT $cvsmodule/debian/changelog > $TEMPFILE + setq "version" \ + "`dpkg-parsechangelog -l$TEMPFILE -SVersion`" \ + "source version" + rm -f "$TEMPFILE" + fi +fi + +rm -f $TEMPFILE +rmdir $TEMPDIR +trap "" 0 1 2 3 7 10 13 15 + + +non_epoch_version=$(echo -n "$version" | perl -pe 's/^\d+://') +upstream_version=$(echo -n "$non_epoch_version" | sed -e 's/-[^-]*$//') +debian_version=$(echo -n $non_epoch_version | perl -nle 'm/-([^-]*)$/ && print $1') + +# The default +if [ "X$opt_rootdir" != "X" ]; then + rootdir="$opt_rootdir" +else + rootdir='/usr/local/src/Packages' +fi + +if [ "X$opt_workdir" != "X" ]; then + workdir="$opt_workdir" +else + workdir="$rootdir/$package" +fi + +# Load site defaults and over rides. +if [ -f /etc/cvsdeb.conf ]; then + . /etc/cvsdeb.conf +fi + +# Load user defaults and over rides. +if [ -f ~/.cvsdeb.conf ]; then + . ~/.cvsdeb.conf +fi + +# Command line, env variable, config file, or default +if [ ! "x$opt_rootdir" = "x" ]; then + rootdir="$opt_rootdir" +elif [ ! "x$CVSDEB_ROOTDIR" = "x" ]; then + rootdir="$CVSDEB_ROOTDIR" +elif [ ! "x$conf_rootdir" = "x" ]; then + rootdir="$conf_rootdir" +fi + +# Command line, env variable, config file, or default +if [ ! "x$opt_workdir" = "x" ]; then + workdir="$opt_workdir" +elif [ ! "x$CVSDEB_WORKDIR" = "x" ]; then + workdir="$CVSDEB_WORKDIR" +elif [ ! "x$conf_workdir" = "x" ]; then + workdir="$conf_workdir" +else + workdir="$rootdir/$package" +fi + +if [ ! -d "$workdir" ]; then + echo >&2 "The working directory, $workdir, does not exist. Aborting." + if [ ! -d "$rootdir" ]; then + echo >&2 "The root directory, $rootdir, does not exist either." + fi + exit 1; +fi + +# The next part is based on debi + +if [ -n "$targetarch" ] && [ -n "$targetgnusystem" ]; then + setq arch "$(dpkg-architecture "-a${targetarch}" "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture" +elif [ -n "$targetarch" ]; then + setq arch "$(dpkg-architecture "-a${targetarch}" -qDEB_HOST_ARCH)" "build architecture" +elif [ -n "$targetgnusystem" ]; then + setq arch "$(dpkg-architecture "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture" +else + setq arch "$(dpkg-architecture -qDEB_HOST_ARCH)" "build architecture" +fi + +pva="${package}_${non_epoch_version}_${arch}" +changes="$pva.changes" + +cd $workdir || { + echo "Couldn't cd $workdir. Aborting" >&2 + exit 1 +} + +if [ ! -r "$changes" ]; then + echo "Can't read $workdir/$changes! Have you built the package yet?" >&2 + exit 1 +fi + +# Just call debc/debi respectively, now that we have a changes file + +SUBPROG="${PROGNAME#cvs-}" + +exec "$SUBPROG" --check-dirname-level 0 $changes "$@" diff --git a/scripts/cvs-debrelease.1 b/scripts/cvs-debrelease.1 new file mode 100644 index 0000000..c911b0d --- /dev/null +++ b/scripts/cvs-debrelease.1 @@ -0,0 +1,72 @@ +.TH CVS-DEBC 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +cvs-debrelease \- upload a cvs-buildpackage/cvs-debuild generated package +.SH SYNOPSIS +\fBcvs-debrelease\fP [\fIcvs-debrelease options\fR] [\fB\-\-dopts\fR +[\fIdupload/dput options\fR]] +.SH DESCRIPTION +\fBcvs-debrelease\fR is run from the CVS working directory after +\fBcvs-buildpackage\fR or \fBcvs-debuild\fR. It uses the +\fBcvs-buildpackage\fR system to locate the \fI.changes\fR file +generated in that run. It then uploads the package using +\fBdebrelease\fR(1), which in turn calls either \fBdupload\fR or +\fBdput\fR. Note that the \fB\-\-dopts\fR option must be specified to +distinguish the \fBcvs-debrelease\fR options from the \fBdupload\fR or +\fBdput\fR options. Also, the \fBdevscripts\fR configuration files +will be read, as described in the \fBdebrelease\fR(1) manpage. +.PP +Note that unlike \fBcvs-buildpackage\fR, the only way to specify the +source package name is with the \fB\-P\fR option; you cannot simply +have it as the last command-line parameter. +.SH OPTIONS +All current \fBcvs-buildpackage\fR options are silently accepted; +however, only the ones listed below have any effect. For more details +on all of them, see the \fBcvs-buildpackage\fR(1) manpage. All +\fBdebrelease\fR options (as listed below) are also accepted. +.TP +\fB\-\-dupload\fR, \fB\-\-dput\fR +This specifies which uploader program to use; the default is +\fBdupload\fR. +.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 +.BR \-M\fImodule +The name of the CVS module. +.TP +.BR \-P\fIpackage +The name of the package. +.TP +.B \-V\fIversion +The version number of the package. +.TP +.B \-T\fItag +The CVS tag to use for exporting sources. +.TP +.B \-R\fIroot\ directory +Root of the original sources archive. +.TP +.B \-W\fIwork directory +The full path name for the cvs-buildpackage working directory. +.TP +.B \-x\fIprefix +This option provides the CVS default module prefix. +.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 "SEE ALSO" +.BR cvs-buildpackage (1), +.BR cvs-debuild (1), +.BR debrelease (1) +.SH AUTHOR +\fBcvs-buildpackage\fR was written by Manoj Srivastava, and the +current version of \fBdebrelease\fR was written by Julian Gilbey +<jdg@debian.org>. They have been combined into this program by +Julian Gilbey. diff --git a/scripts/cvs-debrelease.sh b/scripts/cvs-debrelease.sh new file mode 100755 index 0000000..7fe5ccc --- /dev/null +++ b/scripts/cvs-debrelease.sh @@ -0,0 +1,385 @@ +#!/bin/bash + +# cvs-debrelease: Call dupload/dput to upload package built with +# cvs-buildpackage or cvs-debuild +# +# Based on debrelease; see it for copyright information +# Based on cvs-buildpackage, copyright 1997 Manoj Srivastava +# (CVS Id: cvs-buildpackage,v 1.58 2003/08/22 17:24:29 srivasta Exp) +# This code is copyright 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/>. + +set -e + +PROGNAME=${0##*/} PROGNAME="${PROGNAME%.sh}" # .sh for debugging purposes + +usage() { + echo \ +"Usage: $PROGNAME [cvs-debrelease options] [--dopts [dupload/dput options]] + Upload the .changes file(s) just created by cvs-buildpackage or + cvs-debuild, as listed in the .changes file generated on that run. + + Note that unlike cvs-buildpackage, the only way to specify the + source package name is with the -P option; you cannot simply have it + as the last parameter. + + Also uses the cvs-buildpackage configuration files to determine the + location of the build tree, as described in its manpage. + + Available cvs-debrelease options: + -M<module> CVS module name + -P<package> Package name + -V<version> Package version + -T<tag> CVS tag to use + -R<root dir> Root directory + -W<work dir> Working directory + -x<prefix> CVS default module prefix + -a<arch> Search for .changes file made for Debian build <arch> + -t<target> Search for .changes file made for GNU <target> arch + --dupload Use dupload to upload files (default) + --dput Use dput to upload files + --no-conf, --noconf + Don't read devscripts config files; + must be the first option given + --dopts The remaining options are for dupload/dput + --help Show this message + --version Show version and copyright information + Other cvs-buildpackage options will be silently ignored. + +Default settings modified by devscripts configuration files: + (no configuration files are read by $PROGNAME) +For information on default debrelease settings modified by the +configuration files, run: debrelease --help" +} + + +version() { echo \ +"This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This code is copyright 2003, Julian Gilbey <jdg@debian.org>, +all rights reserved. +Based on original code by Christoph Lameter and Manoj Srivastava. +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." +} + +setq() { + # Variable Value Doc string + if [ "x$2" = "x" ]; then + echo >&2 "$PROGNAME: Unable to determine $3" + exit 1; + else + if [ ! "x$Verbose" = "x" ]; then + echo "$PROGNAME: $3 is $2"; + fi + eval "$1=\"\$2\""; + fi +} + +# Is cvs-buildpackage installed? +if ! command -v cvs-buildpackage > /dev/null; then + echo "$PROGNAME: need the cvs-buildpackage package installed to run this" >&2 + exit 1 +fi + +# Long term variables, which may be set in the cvsdeb config file or the +# environment: +# rootdir workdir (if all original sources are kept in one dir) + +TEMPDIR=$(mktemp -dt cvs-debrelease.XXXXXXXX) || { + echo "$PROGNAME: Unable to create temporary directory" >&2 + echo "Aborting...." >&2 + exit 1 +} +TEMPFILE=$TEMPDIR/cl-tmp +trap 'rm -f "$TEMPFILE"; rmdir "$TEMPDIR"' EXIT + +TAGOPT= + +# Command line +# Start by pulling off all options up to --dopts +declare -a cvsopts debreleaseopts +if [ "$1" = --no-conf -o "$1" = --noconf ]; then + debreleaseopts=("$1") + shift +fi + +debreleaseopts=("${debreleaseopts[@]}" "--check-dirname-level=0") + +while [ $# -gt 0 ]; do + if [ "$1" = "--dopts" ]; then + shift + break + fi + cvsopts=("${cvsopts[@]}" "$1") + shift +done + +# This will bomb out if there is an unrecognised option +TEMP=$(getopt -a -s bash \ + -o hC:EH:G:M:P:R:T:U:V:W:Ff:dcnr:x:Bp:Dk:a:Sv:m:e:i:I:t: \ + --long help,version,ctp,tC,sgpg,spgp,us,uc,op \ + --long si,sa,sd,ap,sp,su,sk,sr,sA,sP,sU,sK,sR,ss,sn \ + --long dupload,dput,no-conf,noconf \ + --long check-dirname-level:,check-dirname-regex: \ + -n "$PROGNAME" -- "${cvsopts[@]}") + +eval set -- $TEMP + +while true ; do + case "$1" in + -h|--help) usage; exit 0 ; shift ;; + --version) version; exit 0 ; shift ;; + -M) opt_cvsmodule="$2" ; shift 2 ;; + -P) opt_package="$2" ; shift 2 ;; + -R) opt_rootdir="$2" ; shift 2 ;; + -T) opt_tag="$2" ; shift 2 ;; + -V) opt_version="$2" ; shift 2 ;; + -W) opt_workdir="$2" ; shift 2 ;; + -x) opt_prefix="$2" ; shift 2 ;; + -a) debreleaseopts=("${debreleaseopts[@]}" "$1" "$2") + targetarch="$2" ; shift 2 ;; + -t) if [ "$2" != "C" ]; then + debreleaseopts=("${debreleaseopts[@]}" "$1" "$2") + targetgnusystem="$2" + fi + shift 2 ;; + --dupload|--dput) + debreleaseopts=("${debreleaseopts[@]}" "$1"); shift ;; + --no-conf|--noconf) + echo "$PROGNAME: $1 is only acceptable as the first command-line option!" >&2 + exit 1 ;; + --check-dirname-level|--check-dirname-regex) + debreleaseopts=("${debreleaseopts[@]}" "$1" "$2"); shift 2 ;; + + # everything else is silently ignored + -[CHfGUr]) shift 2 ;; + -[FnE]) shift ;; + --ctp|--op|--tC) shift ;; + -[dDBbS]) shift ;; + -p) shift 2 ;; + --us|--uc|--sgpg|--spgp) shift ;; + --s[idapukrAPUKRns]) shift ;; + --ap) shift ;; + -[kvmeiI]) shift 2 ;; + + --) shift ; break ;; + *) echo >&2 "Internal error! ($1)" + usage; exit 1 ;; + esac +done + +if [ "x$opt_cvsmodule" = "x" -a "x$opt_package" = "x" -a \ + ! -e 'debian/changelog' ] ; then + echo >&2 "$PROGNAME should be run in the top working directory of" + echo >&2 "a Debian Package, or an explicit package (or CVS module) name" + echo >&2 "should be given." + exit 1 +fi + +if [ "x$opt_tag" != "x" ]; then + TAGOPT=-r$opt_tag +fi + +# Command line, env variable, config file, or default +# This anomalous position is in case we need to check out the changelog +# below (anomalous since we have not loaded the config file yet) +if [ ! "x$opt_prefix" = "x" ]; then + prefix="$opt_prefix" +elif [ ! "x$CVSDEB_PREFIX" = "x" ]; then + prefix="$CVSDEB_PREFIX" +elif [ ! "x$conf_prefix" = "x" ]; then + prefix="$conf_prefix" +else + prefix="" +fi + +# put a slash at the end of the prefix +if [ "X$prefix" != "X" ]; then + prefix="$prefix/"; + prefix=$(echo $prefix | sed 's://:/:g'); +fi + +if [ ! -f CVS/Root ]; then + if [ "X$CVSROOT" = "X" ]; then + echo "no CVS/Root file found, and CVSROOT var is empty" >&2 + exit 1 + fi +else + CVSROOT=$(cat CVS/Root) + export CVSROOT +fi + +if [ "x$opt_package" = "x" ]; then + # Get the official package name and version. + if [ -f debian/changelog ]; then + # Ok, changelog exists + setq "package" \ + "`dpkg-parsechangelog -SSource`" \ + "source package" + setq "version" \ + "`dpkg-parsechangelog -SVersion`" \ + "source version" + elif [ "x$opt_cvsmodule" != "x" ]; then + # Hmm. Well, see if we can checkout the changelog file + rm -f $TEMPFILE + cvs -q co -p $TAGOPT $opt_cvsmodule/debian/changelog > $TEMPFILE + setq "package" \ + "`dpkg-parsechangelog -l$TEMPFILE -SSource`" \ + "source package" + setq "version" \ + "`dpkg-parsechangelog -l$TEMPFILE -SVersion`" \ + "source version" + rm -f "$TEMPFILE" + else + # Well. We don't know what this package is. + echo >&2 " This does not appear be a Debian source tree, since" + echo >&2 " there is no debian/changelog, and there was no" + echo >&2 " package name or cvs module given on the command line" + echo >&2 " it is hard to figure out what the package name " + echo >&2 " should be. I give up." + exit 1 + fi +else + # The user knows best; package name is provided + setq "package" "$opt_package" "source package" + + # Now, the version number + if [ "x$opt_version" != "x" ]; then + # All hail the user provided value + setq "version" "$opt_version" "source package" + elif [ -f debian/changelog ]; then + # Fine, see what the changelog says + setq "version" \ + "`dpkg-parsechangelog -SVersion`" \ + "source version" + elif [ "x$opt_cvsmodule" != "x" ]; then + # Hmm. The CVS module name is known, so lets us try exporting changelog + rm -f $TEMPFILE + cvs -q co -p $TAGOPT $opt_cvsmodule/debian/changelog > $TEMPFILE + setq "version" \ + "`dpkg-parsechangelog -l$TEMPFILE -SVersion`" \ + "source version" + rm -f "$TEMPFILE" + else + # Ok, try exporting the package name + rm -f $TEMPFILE + cvsmodule="${prefix}$package" + cvs -q co -p $TAGOPT $cvsmodule/debian/changelog > $TEMPFILE + setq "version" \ + "`dpkg-parsechangelog -l$TEMPFILE -SVersion`" \ + "source version" + rm -f "$TEMPFILE" + fi +fi + +rm -f $TEMPFILE +rmdir $TEMPDIR +trap "" 0 1 2 3 7 10 13 15 + + +non_epoch_version=$(echo -n "$version" | perl -pe 's/^\d+://') +upstream_version=$(echo -n "$non_epoch_version" | sed -e 's/-[^-]*$//') +debian_version=$(echo -n $non_epoch_version | perl -nle 'm/-([^-]*)$/ && print $1') + +# The default +if [ "X$opt_rootdir" != "X" ]; then + rootdir="$opt_rootdir" +else + rootdir='/usr/local/src/Packages' +fi + +if [ "X$opt_workdir" != "X" ]; then + workdir="$opt_workdir" +else + workdir="$rootdir/$package" +fi + +# Load site defaults and over rides. +if [ -f /etc/cvsdeb.conf ]; then + . /etc/cvsdeb.conf +fi + +# Load user defaults and over rides. +if [ -f ~/.cvsdeb.conf ]; then + . ~/.cvsdeb.conf +fi + +# Command line, env variable, config file, or default +if [ ! "x$opt_rootdir" = "x" ]; then + rootdir="$opt_rootdir" +elif [ ! "x$CVSDEB_ROOTDIR" = "x" ]; then + rootdir="$CVSDEB_ROOTDIR" +elif [ ! "x$conf_rootdir" = "x" ]; then + rootdir="$conf_rootdir" +fi + +# Command line, env variable, config file, or default +if [ ! "x$opt_workdir" = "x" ]; then + workdir="$opt_workdir" +elif [ ! "x$CVSDEB_WORKDIR" = "x" ]; then + workdir="$CVSDEB_WORKDIR" +elif [ ! "x$conf_workdir" = "x" ]; then + workdir="$conf_workdir" +else + workdir="$rootdir/$package" +fi + +if [ ! -d "$workdir" ]; then + echo >&2 "The working directory, $workdir, does not exist. Aborting" + if [ ! -d "$rootdir" ]; then + echo >&2 "The root directory, $rootdir, does not exist either." + fi + exit 1; +fi + +pkgdir="$workdir/$package-$upstream_version" + +if [ ! -d "$pkgdir" ]; then + echo "The build directory $pkgdir does not exist!" >&2 + echo "Have you built the package yet?" >&2 + exit 1 +fi + +if [ -n "$targetarch" ] && [ -n "$targetgnusystem" ]; then + setq arch "$(dpkg-architecture "-a${targetarch}" "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture" +elif [ -n "$targetarch" ]; then + setq arch "$(dpkg-architecture "-a${targetarch}" -qDEB_HOST_ARCH)" "build architecture" +elif [ -n "$targetgnusystem" ]; then + setq arch "$(dpkg-architecture "-t${targetgnusystem}" -qDEB_HOST_ARCH)" "build architecture" +else + setq arch "$(dpkg-architecture -qDEB_HOST_ARCH)" "build architecture" +fi + +pva="${package}_${non_epoch_version}_${arch}" +changes="$pva.changes" + +if [ ! -f "$workdir/$changes" ]; then + echo "Can't find $workdir/$changes!" >&2 + echo "Have you built the package yet?" >&2 + exit 1 +fi + + +cd $pkgdir || { + echo "Couldn't cd $pkgdir. Aborting" >&2 + exit 1 +} + +# Just call debrelease, now that we are in the correct directory + +SUBPROG="${PROGNAME#cvs-}" + +exec "$SUBPROG" "${debreleaseopts[@]}" "$@" diff --git a/scripts/cvs-debuild.1 b/scripts/cvs-debuild.1 new file mode 100644 index 0000000..bd4712a --- /dev/null +++ b/scripts/cvs-debuild.1 @@ -0,0 +1,59 @@ +.TH CVS-DEBUILD 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +cvs-debuild \- build a Debian package using cvs-buildpackage and debuild +.SH SYNOPSIS +\fBcvs-debuild\fR [\fIdebuild options\fR] [\fIcvs-buildpackage options\fR] +[\fB\-\-lintian-opts\fR \fIlintian options\fR] +.SH DESCRIPTION +\fBcvs-debuild\fR is a wrapper around \fBcvs-buildpackage\fR to run it +with \fBdebuild\fR as the package-building program. (This cannot +simply be accomplished using the \fB\-C\fR option of +\fBcvs-buildpackage\fR, as it does not know how to handle all of the +special \fBdebuild\fR options.) +.PP +The program simply stashes the \fBdebuild\fR and \fBlintian\fR +options, and passes them to \fBdebuild\fR when it is +called by \fBcvs-buildpackage\fR. All of the standard \fBdebuild\fR +options may be used (as listed below), but note that the root command +specified by any \fB\-\-rootcmd\fR or \fB\-r\fR command-line option +will be passed as an option to \fBcvs-buildpackage\fR. The first +non-\fBdebuild\fR option detected will signal the start of the +\fBcvs-buildpackage\fR options. +.PP +The selection of the root command is slightly subtle: if there are any +command-line options, these will be used. If not, then if +\fBcvs-buildpackage\fR is set up to use a default root command, that +will be used. Finally, if neither of these are the case, then +\fBdebuild\fR will use its procedures to determine an appropriate +command, as described in its documentation. +.PP +See the manpages for \fBdebuild\fR(1) and \fBcvs-buildpackage\fR for +more information about the behaviour of each. +.SH "OPTIONS" +The following are the \fBdebuild\fR options recognised by +\fBcvs-debuild\fR. All \fBcvs-buildpackage\fR and \fBlintian\fR +options are simply passed to the appropriate program. For +explanations of the meanings of these variables, see +\fBdebuild\fR(1). +.TP +.B \-\-no\-conf\fR, \fB\-\-noconf +.TP +.BI \-\-rootcmd= "gain-root-command\fR, \fP" \-r gain-root-command +.TP +.B \-\-preserve\-env +.TP +.BI \-\-preserve\-envvar= "var\fR, \fP" \-e var +.TP +.BI \-\-set\-envvar= var = "value\fR, \fP" \-e var = value +.TP +.B \-\-lintian\fR, \fB\-\-no\-lintian +.TP +\fB\-\-ignore-dirname\fR, \fB\-\-check-dirname\fR +These should not be needed, but it is provided nevertheless. +.SH "SEE ALSO" +.BR cvs-buildpackage (1), +.BR debuild (1), +.BR dpkg-buildpackage (1), +.BR lintian (1) +.SH AUTHOR +This program was written by Julian Gilbey <jdg@debian.org>. diff --git a/scripts/cvs-debuild.pl b/scripts/cvs-debuild.pl new file mode 100755 index 0000000..4a33f0f --- /dev/null +++ b/scripts/cvs-debuild.pl @@ -0,0 +1,216 @@ +#!/usr/bin/perl + +# A wrapper for cvs-buildpackage to use debuild, still giving access +# to all of debuild's functionality. + +# Copyright 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/>. + +# We will do simple option processing. The calling syntax of this +# program is: +# +# cvs-debuild [<debuild options>] [<cvs-buildpackage options>] +# [--lintian-opts <lintian options>] +# +# cvs-debuild will run cvs-buildpackage, using debuild as the +# package-building program, passing the debuild and lintian options to +# it. For details of these options, and more information on debuild in +# general, refer to debuild(1). + +use 5.006; +use strict; +use warnings; +use FileHandle; +use File::Basename; +use File::Temp qw/ tempfile /; +use Fcntl; + +my $progname = basename($0); + +# Predeclare functions +sub fatal($); + +sub usage { + print <<"EOF"; + $progname [<debuild options>] [<cvs-buildpackage options>] + [--lintian-opts <lintian options>] + to run cvs-buildpackage using debuild as the package building program + + Accepted debuild options, see debuild(1) or debuild --help for more info: + --no-conf, --noconf + --lintian, --no-lintian + --rootcmd=<gain-root-command>, -r<gain-root-command> + --preserve-envvar=<envvar>, -e<envvar> + --set-envvar=<envvar>=<value>, -e<envvar>=<value> + --preserve-env + --check-dirname-level=<value>, --check-dirname-regex=<regex> + -d, -D + + --help display this message + --version show version and copyright information + All cvs-buildpackage options are accepted, as are all lintian options. + + Note that any cvs-buildpackage options (command line or configuration file) + for setting a root command will override any debuild configuration file + options for this. + +Default settings modified by devscripts configuration files: + (no configuration files are read by $progname) +For information on default debuild settings modified by the +configuration files, run: debuild --help +EOF +} + +sub version { + print <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +This code is copyright 2003 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 +} + +# First check we can execute cvs-buildpackage +unless (system("command -v cvs-buildpackage >/dev/null 2>&1") == 0) { + fatal "can't run cvs-buildpackage; have you installed it?"; +} + +# We start by parsing the command line to collect debuild and +# lintian options. We stash them away in temporary files, +# which we will pass to debuild. + +my (@debuild_opts, @cvs_opts, @lin_opts); +{ + no locale; + # debuild opts first + while (@ARGV) { + my $arg = shift; + $arg eq '--help' and usage(), exit 0; + $arg eq '--version' and version(), exit 0; + + # rootcmd gets passed on to cvs-buildpackage + if ($arg eq '-r' or $arg eq '--rootcmd') { + push @cvs_opts, '-r' . shift; + next; + } + if ($arg =~ /^(?:-r|--rootcmd=)(.*)$/) { + push @cvs_opts, "-r$1"; + next; + } + + # other debuild options are stashed + if ($arg =~ /^--(no-?conf|(no-?)?lintian)$/) { + push @debuild_opts, $arg; + next; + } + if ($arg =~ /^--preserve-env$/) { + push @debuild_opts, $arg; + next; + } + if ($arg =~ /^--check-dirname-(level|regex)$/) { + push @debuild_opts, $arg, shift; + next; + } + if ($arg =~ /^--check-dirname-(level|regex)=/) { + push @debuild_opts, $arg; + next; + } + if ($arg =~ /^--(preserve|set)-envvar$/) { + push @debuild_opts, $arg, shift; + next; + } + if ($arg =~ /^--(preserve|set)-envvar=/) { + push @debuild_opts, $arg; + next; + } + # dpkg-buildpackage now has a -e option, so we have to be + # careful not to confuse the two; their option will always have + # the form -e<maintainer email> or similar + if ($arg eq '-e') { + push @debuild_opts, $arg, shift; + next; + } + if ($arg =~ /^-e(\w+(=.*)?)$/) { + push @debuild_opts, $arg; + next; + } + if ($arg eq '-d' or $arg eq '-D') { + push @debuild_opts, $arg; + next; + } + # Anything else matching /^-e/ is a dpkg-buildpackage option, + # and we've also now considered all debuild options. + # So now handle cvs-buildpackage options + unshift @ARGV, $arg; + last; + } + + while (@ARGV) { + my $arg = shift; + if ($arg eq '-L' or $arg eq '--lintian') { + fatal "$arg argument not recognised; use --lintian-opts instead"; + } + if ($arg =~ /^--lin(tian|da)-opts$/) { + push @lin_opts, $arg; + last; + } + push @cvs_opts, $arg; + } + + if (@ARGV) { + push @lin_opts, @ARGV; + } +} + +# So we've now got three arrays, and we'll have to store the debuild +# options in temporary files +my $debuild_cmd = 'debuild --cvs-debuild'; +my ($fhdeb, $fhlin); +if (@debuild_opts) { + $fhdeb = tempfile("cvspreXXXXXX", UNLINK => 1) + or fatal "cannot create temporary file: $!"; + fcntl $fhdeb, Fcntl::F_SETFD(), 0 + or fatal "disabling close-on-exec for temporary file: $!"; + print $fhdeb join("\0", @debuild_opts); + $debuild_cmd .= ' --cvs-debuild-deb /dev/fd/' . fileno($fhdeb); +} +if (@lin_opts) { + $fhlin = tempfile("cvspreXXXXXX", UNLINK => 1) + or fatal "cannot create temporary file: $!"; + fcntl $fhlin, Fcntl::F_SETFD(), 0 + or fatal "disabling close-on-exec for temporary file: $!"; + print $fhlin join("\0", @lin_opts); + $debuild_cmd .= ' --cvs-debuild-lin /dev/fd/' . fileno($fhlin); +} + +# Now we can run cvs-buildpackage +my $status = system('cvs-buildpackage', '-C' . $debuild_cmd, @cvs_opts); + +if ($status & 255) { + die "cvs-debuild: cvs-buildpackage terminated abnormally: " + . sprintf("%#x", $status) . "\n"; +} else { + exit($status >> 8); +} + +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/; + die $msg; +} diff --git a/scripts/dcmd.1 b/scripts/dcmd.1 new file mode 100644 index 0000000..0438082 --- /dev/null +++ b/scripts/dcmd.1 @@ -0,0 +1,112 @@ +.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 \-\-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. diff --git a/scripts/dcmd.sh b/scripts/dcmd.sh new file mode 100755 index 0000000..927baf8 --- /dev/null +++ b/scripts/dcmd.sh @@ -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 diff --git a/scripts/dd-list.1 b/scripts/dd-list.1 new file mode 100644 index 0000000..61f9231 --- /dev/null +++ b/scripts/dd-list.1 @@ -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> diff --git a/scripts/dd-list.pl b/scripts/dd-list.pl new file mode 100755 index 0000000..d1dbcfb --- /dev/null +++ b/scripts/dd-list.pl @@ -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); diff --git a/scripts/deb-janitor b/scripts/deb-janitor new file mode 100755 index 0000000..9f5f242 --- /dev/null +++ b/scripts/deb-janitor @@ -0,0 +1,320 @@ +#!/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 + logging.info("Status: %s - %s", data["result_code"], data["description"]) + logging.info("Command: %s", data["command"]) + if data.get("failure"): + logging.warning("Failure stage: %s", data["failure"]) + 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:])) diff --git a/scripts/deb-janitor.1 b/scripts/deb-janitor.1 new file mode 100644 index 0000000..223a206 --- /dev/null +++ b/scripts/deb-janitor.1 @@ -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) diff --git a/scripts/deb-reversion.dbk b/scripts/deb-reversion.dbk new file mode 100644 index 0000000..942f4e9 --- /dev/null +++ b/scripts/deb-reversion.dbk @@ -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: +--> diff --git a/scripts/deb-reversion.sh b/scripts/deb-reversion.sh new file mode 100755 index 0000000..88ee290 --- /dev/null +++ b/scripts/deb-reversion.sh @@ -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 diff --git a/scripts/deb-why-removed.pl b/scripts/deb-why-removed.pl new file mode 100755 index 0000000..1c496b4 --- /dev/null +++ b/scripts/deb-why-removed.pl @@ -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 diff --git a/scripts/debbisect b/scripts/debbisect new file mode 100755 index 0000000..d4a42c7 --- /dev/null +++ b/scripts/debbisect @@ -0,0 +1,1229 @@ +#!/usr/bin/env python3 +# +# Copyright 2020 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. + +# snapshot.d.o API feature requests: +# +# Currently, the API does not allow to list all dates for which a snapshot +# was made. This would be useful to allow skipping snapshots. Currently we +# blindly bisect but without knowing which date on snapshot.d.o a given +# timestamp resolves to, we cannot mark it as untestable (see feature request +# above) and without a list of testable timestamps we cannot reliably test +# a timestamp before and after the one to skip. +# See also: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=969603 +# +# It would be useful to know when a package version was first seen in a given +# suite. Without this knowledge we cannot reliably pick the snapshot timestamps +# at which we want to test a given suite. For example, a package version might +# appear in experimental long before it appears in unstable or any other suite +# that is to be tested. Thus, the first_seen attribute of the snapshot API is +# not very useful to determine a limited set of timestamps to test. +# See also: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=806329 + +# wishlist features +# ----------------- +# +# - restrict set of tested snapshot timestamps to those where a given package +# set actually changed (needs a resolution to #806329) +# +# - allow marking snapshot timestamps as skippable, for example via script +# exit code (needs resolution to #969603) +# +# - add convenience function which builds a given source package and installs +# its build dependencies automatically + +# complains about log_message cannot be fixed because the original function +# names one of its arguments "format" which is also forbidden... +# pylint: disable=W0221 +# +# pylint complains about too many branches but the code would not become more +# readable by spreading it out across more functions +# pylint: disable=R0912 +# +# allow more than 1000 lines in this file +# pylint: disable=C0302 +# +# TODO: Adress invalid names +# pylint: disable=invalid-name + +import argparse +import atexit +import collections +import email.utils +import http.client +import http.server +import io +import logging +import lzma +import math +import os +import pty +import re +import select +import shutil +import socketserver +import subprocess +import sys +import tempfile +import threading +import urllib.error +import urllib.request +from datetime import date, datetime, time, timedelta, timezone +from functools import partial +from http import HTTPStatus +from time import sleep + +import debian +import debian.deb822 +import requests + +HAVE_DATEUTIL = True +try: + import dateutil.parser +except ImportError: + HAVE_DATEUTIL = False + +HAVE_PARSEDATETIME = True +try: + import parsedatetime +except ImportError: + HAVE_PARSEDATETIME = False + +DINSTALLRATE = 21600 + + +def format_timestamp(timestamp): + return timestamp.strftime("%Y%m%dT%H%M%SZ") + + +# We utilize the fact that the HTTP interface of snapshot.d.o responds with a +# HTTP 301 redirect to the corresponding timestamp. +# It would be better if there as an officially documented API function: +# http://bugs.debian.org/969605 +def sanitize_timestamp(timestamp): + conn = http.client.HTTPConnection("snapshot.debian.org") + conn.request( + "HEAD", "/archive/debian/" + timestamp.strftime("%Y%m%dT%H%M%SZ") + "/" + ) + res = conn.getresponse() + if res.status == 200: + return timestamp + assert res.status in (301, 302), res.status # moved permanently or temporarily + prefix = "http://snapshot.debian.org/archive/debian/" + location = res.headers["Location"] + assert location.startswith(prefix) + # flake8 wrongly insists that there must be no whitespace before colon + # See https://github.com/PyCQA/pycodestyle/issues/373 + location = location[len(prefix) :] # noqa: E203 + return datetime.strptime(location, "%Y%m%dT%H%M%S%z/") + + +# 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 +class Proxy(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + # check validity and extract the timestamp + try: + c1, c2, c3, timestamp, _ = self.path.split("/", 4) + 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"] != [c1, c2, c3]: + 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, c2, c3, timestamp, "pool") + if not os.path.exists(linkname): + os.makedirs(os.path.join(self.directory, c2, c3, timestamp), exist_ok=True) + os.symlink("../../../pool", linkname) + path = os.path.abspath(self.translate_path(self.path)) + if not os.path.exists(path): + self._download_new(path) + return + f = self.send_head() + if f: + try: + self.copyfile(f, self.wfile) + except ConnectionResetError: + pass + f.close() + + def _download_new(self, path): + # save file in local cache + maxtries = 3 + head, _ = os.path.split(path) + os.makedirs(head, exist_ok=True) + totalsize = -1 + downloaded = 0 + for trynum in range(maxtries): + try: + headers = {} + if downloaded > 0: + # if file was partly downloaded, only request the rest + headers["Range"] = f"bytes={downloaded}-" + req = urllib.request.Request( + "http://snapshot.debian.org/" + self.path, headers=headers + ) + # we use os.fdopen(os.open(...)) because we don't want to + # truncate the file and seek to the right position but also + # create it if it doesn't exist yet + with urllib.request.urlopen(req) as f, os.fdopen( + os.open(path, os.O_RDWR | os.O_CREAT), "rb+" + ) as out: + out.seek(downloaded) + if trynum == 0: + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", f.headers["Content-type"]) + self.send_header("Content-Length", f.headers["Content-Length"]) + self.send_header("Last-Modified", f.headers["Last-Modified"]) + self.end_headers() + totalsize = int(f.headers["Content-Length"]) + while downloaded < totalsize: + chunksize = 200 * 1024 + if totalsize - downloaded < chunksize: + chunksize = totalsize - downloaded + buf = f.read(chunksize) # 200 kB/s + if len(buf) != chunksize: + # something went wrong + logging.warning( + "%s: wanted %d but got %d bytes (try %d of %d)", + path, + chunksize, + len(buf), + trynum + 1, + maxtries, + ) + sleep(10) + break + sleep(1) # snapshot.d.o needs heavy throttling + out.write(buf) + self.wfile.write(buf) + downloaded += chunksize + except urllib.error.HTTPError as e: + if e.code == 404: + self.send_error(404, "URLError") + return + logging.warning("got urllib.error.HTTPError: %s %s", repr(e), self.path) + except urllib.error.URLError as e: + logging.warning("got urllib.error.URLError: %s", repr(e)) + if downloaded == totalsize: + break + if totalsize != downloaded: + if os.path.exists(path): + os.unlink(path) + self.send_error(500, "URLError") + return + + def log_message(self, fmt, *args): + pass + + +def srcpkgversions_by_timestamp(srcpkgname, timestamp, suite): + versions = set() + timestamp_str = timestamp.strftime("%Y%m%dT%H%M%SZ") + r = requests.get( + f"http://snapshot.debian.org/archive/debian/{timestamp_str}" + f"/dists/{suite}/main/source/Sources.xz", + timeout=60, + ) + data = lzma.decompress(r.content) + for src in debian.deb822.Sources.iter_paragraphs(io.BytesIO(data)): + if src["Package"] != srcpkgname: + continue + versions.add(debian.debian_support.Version(src["Version"])) + return versions + + +def binpkgversion_by_timestamp(binpkgname, timestamp, suite, architecture): + timestamp_str = timestamp.strftime("%Y%m%dT%H%M%SZ") + r = requests.get( + f"http://snapshot.debian.org/archive/debian/{timestamp_str}" + f"/dists/{suite}/main/binary-{architecture}/Packages.xz", + timeout=60, + ) + data = lzma.decompress(r.content) + for pkg in debian.deb822.Packages.iter_paragraphs(io.BytesIO(data)): + if pkg["Package"] == binpkgname: + return debian.debian_support.Version(pkg["Version"]) + return None + + +# This function does something similar to what this wiki page describes +# https://wiki.debian.org/BisectDebian#Finding_dates_for_specific_packages +# +# The problem with the approach on that wiki page as well as the one below in +# Python is, that it relies on the first_seen entry provided by snapshot.d.o. +# This means that we do not know when a package first appeared in a given +# suite. It could've first appeared in experimental or even in Debian Ports. +# +# Also see: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=806329 +def first_seen_by_pkg(packages, timestamp_begin, timestamp_end, suite, architecture): + timestamps = set() + for pkg in packages: + logging.info("obtaining versions for %s", pkg) + if pkg.startswith("src:"): + pkg = pkg[4:] + oldest_versions = srcpkgversions_by_timestamp(pkg, timestamp_begin, suite) + if len(oldest_versions) == 0: + logging.error( + "source package %s cannot be found in good timestamp", pkg + ) + sys.exit(1) + elif len(oldest_versions) == 1: + oldest_version = oldest_versions.pop() + else: + oldest_version = min(oldest_versions) + newest_versions = srcpkgversions_by_timestamp(pkg, timestamp_end, suite) + if len(newest_versions) == 0: + logging.error("source package %s cannot be found in bad timestamp", pkg) + sys.exit(1) + elif len(newest_versions) == 1: + newest_version = newest_versions.pop() + else: + newest_version = max(newest_versions) + + for result in requests.get( + f"http://snapshot.debian.org/mr/package/{pkg}/", timeout=60 + ).json()["result"]: + if debian.debian_support.Version(result["version"]) < oldest_version: + continue + if debian.debian_support.Version(result["version"]) > newest_version: + continue + r = requests.get( + f"http://snapshot.debian.org/mr/package/{pkg}" + f"/{result['version']}/allfiles?fileinfo=1", + timeout=60, + ) + logging.info("retrieving for: %s", result["version"]) + for fileinfo in [ + fileinfo + for fileinfos in r.json()["fileinfo"].values() + for fileinfo in fileinfos + ]: + if fileinfo["archive_name"] != "debian": + continue + timestamps.add( + datetime.strptime(fileinfo["first_seen"], "%Y%m%dT%H%M%S%z") + ) + else: + oldest_version = binpkgversion_by_timestamp( + pkg, timestamp_begin, suite, architecture + ) + if oldest_version is None: + logging.error( + "binary package %s cannot be found in good timestamp", pkg + ) + sys.exit(1) + newest_version = binpkgversion_by_timestamp( + pkg, timestamp_end, suite, architecture + ) + if newest_version is None: + logging.error("binary package %s cannot be found in bad timestamp", pkg) + sys.exit(1) + r = requests.get(f"http://snapshot.debian.org/mr/binary/{pkg}/", timeout=60) + for result in r.json()["result"]: + if debian.debian_support.Version(result["version"]) < oldest_version: + continue + if debian.debian_support.Version(result["version"]) > newest_version: + continue + r = requests.get( + f"http://snapshot.debian.org/mr/binary/{pkg}" + f"/{result['version']}/binfiles?fileinfo=1", + timeout=60, + ) + logging.info("retrieving for: %s", result["version"]) + hashes = [ + e["hash"] + for e in r.json()["result"] + if e["architecture"] == architecture + ] + for fileinfo in [ + fileinfo for h in hashes for fileinfo in r.json()["fileinfo"][h] + ]: + if fileinfo["archive_name"] != "debian": + continue + timestamps.add( + datetime.strptime(fileinfo["first_seen"], "%Y%m%dT%H%M%S%z") + ) + return timestamps + + +def get_mirror(port, timestamp): + timestamp_str = timestamp.strftime("%Y%m%dT%H%M%SZ") + if port is not None: + return f"http://127.0.0.1:{port}/archive/debian/{timestamp_str}" + return f"http://snapshot.debian.org/archive/debian/{timestamp_str}" + + +def runtest_cmd(cmd, env): + ret = 0 + output = b"" + try: + # we only use the more complex Popen method if live output is required + # for logging levels of INFO or lower + if logging.root.isEnabledFor(logging.INFO): + parent_fd, child_fd = pty.openpty() + with subprocess.Popen( + cmd, + stdin=child_fd, + stderr=child_fd, + stdout=child_fd, + close_fds=True, + env=env, + ) as process: + buf = io.BytesIO() + os.close(child_fd) + while process.poll() is None: + ready, _, _ = select.select([parent_fd], [], [], 1) + if parent_fd in ready: + try: + data = os.read(parent_fd, 10240) + except OSError: + break + if not data: + break # EOF + os.write(sys.stdout.fileno(), data) + buf.write(data) + os.close(parent_fd) + ret = process.wait() + output = buf.getvalue() + else: + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, env=env) + except subprocess.CalledProcessError as e: + ret = e.returncode + output = e.output + return (ret, output) + + +def runtest(timestamp, staticargs, toupgrade=None, badtimestamp=None): + goodmirror = get_mirror(staticargs.port, timestamp) + env = {k: v for k, v in os.environ.items() if k.startswith("DEBIAN_BISECT_")} + env["DEBIAN_BISECT_EPOCH"] = str(int(timestamp.timestamp())) + env["DEBIAN_BISECT_TIMESTAMP"] = timestamp.strftime("%Y%m%dT%H%M%SZ") + env["PATH"] = os.environ.get("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") + if staticargs.port is not None: + env["DEBIAN_BISECT_MIRROR"] = goodmirror + if staticargs.depends or staticargs.qemu: + scriptname = "run_bisect" + if staticargs.qemu: + scriptname = "run_bisect_qemu" + # first try run_bisect.sh from the directory where debbisect lives in + # case we run this from a git clone + run_bisect = os.path.join( + os.path.dirname(os.path.realpath(__file__)), scriptname + ".sh" + ) + if not os.path.exists(run_bisect): + run_bisect = os.path.join("/usr/share/devscripts/scripts/", scriptname) + cmd = [run_bisect] + if staticargs.depends is not None: + cmd.append(staticargs.depends) + else: + cmd.append("") + cmd.extend( + [ + staticargs.script, + goodmirror, + staticargs.architecture, + staticargs.suite, + staticargs.components, + ] + ) + if staticargs.qemu: + cmd.extend([staticargs.qemu["memsize"], staticargs.qemu["disksize"]]) + if toupgrade: + cmd.extend([get_mirror(staticargs.port, badtimestamp), toupgrade]) + else: + # execute it directly if it's an executable file or if it there are no + # shell metacharacters + if ( + os.access(staticargs.script, os.X_OK) + or re.search(r"[^\w@\%+=:,.\/-]", staticargs.script, re.ASCII) is None + ): + cmd = [staticargs.script] + else: + cmd = ["sh", "-c", staticargs.script] + return runtest_cmd(cmd, env) + + +def get_log_fname(timestamp, goodbad, toupgrade=None): + if toupgrade is None: + return f"debbisect.{timestamp.strftime('%Y%m%dT%H%M%SZ')}.log.{goodbad}" + return f"debbisect.{timestamp.strftime('%Y%m%dT%H%M%SZ')}.{toupgrade}.log.{goodbad}" + + +def write_log_symlink(goodbad, output, timestamp, toupgrade=None): + fname = get_log_fname(timestamp, goodbad, toupgrade) + with open(fname, "wb") as f: + f.write(output) + if goodbad == "good": + if os.path.lexists("debbisect.log.good"): + os.unlink("debbisect.log.good") + os.symlink(fname, "debbisect.log.good") + elif goodbad == "bad": + if os.path.lexists("debbisect.log.bad"): + os.unlink("debbisect.log.bad") + os.symlink(fname, "debbisect.log.bad") + + +def bisect(good, bad, staticargs): + # no idea how to split this function into parts without making it + # unreadable + # pylint: disable=too-many-statements + diff = bad - good + print(f"snapshot timestamp difference: {diff / timedelta(days=1)} days") + + stepnum = 1 + starttime = datetime.now(timezone.utc) + + steps = round( + (math.log(diff.total_seconds()) - math.log(DINSTALLRATE)) / math.log(2) + 2 + ) + print(f"approximately {steps} steps left to test") + # verify that the good timestamp is really good and the bad timestamp is really bad + # we try the bad timestamp first to make sure that the problem exists + if not staticargs.ignore_cached_results and os.path.exists( + get_log_fname(good, "good") + ): + print(f"#{stepnum}: using cached results from {get_log_fname(good, 'good')}") + else: + print(f"#{stepnum}: trying known good {format_timestamp(good)}...") + ret, output = runtest(good, staticargs) + if ret != 0: + write_log_symlink("bad", output, good) + print( + "good timestamp was actually bad -- see debbisect.log.bad for details" + ) + return None + write_log_symlink("good", output, good) + stepnum += 1 + steps = round( + (math.log(diff.total_seconds()) - math.log(DINSTALLRATE)) / math.log(2) + 1 + ) + timeleft = steps * (datetime.now(timezone.utc) - starttime) / (stepnum - 1) + print(f"computation time left: {timeleft}") + print(f"approximately {steps} steps left to test") + if not staticargs.ignore_cached_results and os.path.exists( + get_log_fname(bad, "bad") + ): + print(f"#{stepnum}: using cached results from {get_log_fname(bad, 'bad')}") + else: + print(f"#{stepnum}: trying known bad {format_timestamp(bad)}...") + ret, output = runtest(bad, staticargs) + if ret == 0: + write_log_symlink("good", output, bad) + print( + "bad timestamp was actually good -- see debbisect.log.good for details" + ) + return None + write_log_symlink("bad", output, bad) + stepnum += 1 + + while True: + diff = bad - good + # One may be tempted to try and optimize this step by finding all the + # packages that differ between the two timestamps and then finding + # all the snapshot timestamps where the involved packages changed + # in their version. But since dependencies can arbitrarily change + # between two given timestamps, drawing in more packages or requiring + # less packages, the only reliable method is really to strictly bisect + # by taking the timestamp exactly between the two and not involve + # other guessing magic. + newts = sanitize_timestamp(good + diff / 2) + if newts in [good, bad]: + # If the middle timestamp mapped onto good or bad, then the + # timestamps are very close to each other. Test if there is maybe + # not another one between them by sanitizing the timestamp one + # second before the bad one + newts = sanitize_timestamp(bad - timedelta(seconds=1)) + if newts == good: + break + print(f"snapshot timestamp difference: {diff / timedelta(days=1)} days") + steps = round( + (math.log(diff.total_seconds()) - math.log(DINSTALLRATE)) / math.log(2) + 0 + ) + timeleft = steps * (datetime.now(timezone.utc) - starttime) / (stepnum - 1) + print(f"computation time left: {timeleft}") + print(f"approximately {steps} steps left to test") + if not staticargs.ignore_cached_results and os.path.exists( + get_log_fname(newts, "good") + ): + print( + f"#{stepnum}: using cached result (was good)" + f" from {get_log_fname(newts, 'good')}" + ) + good = newts + elif not staticargs.ignore_cached_results and os.path.exists( + get_log_fname(newts, "bad") + ): + print( + f"#{stepnum}: using cached result (was bad)" + f" from {get_log_fname(newts, 'bad')}" + ) + bad = newts + else: + print(f"#{stepnum}: trying {format_timestamp(newts)}...") + ret, output = runtest(newts, staticargs) + if ret == 0: + print("test script output: good") + write_log_symlink("good", output, newts) + good = newts + else: + print("test script output: bad") + write_log_symlink("bad", output, newts) + bad = newts + stepnum += 1 + return good, bad + + +def datetimestr(val): + if val == "now": + return datetime.now(timezone.utc) + if val == "today": + return datetime.combine(date.today(), time(0, 0, 0, 0), timezone.utc) + + # since py3 we don't need pytz to figure out the local timezone + localtz = datetime.now(timezone.utc).astimezone().tzinfo + + # first trying known formats + for fmt in [ + "%Y%m%dT%H%M%SZ", # snapshot.debian.org style + # ISO 8601 + "%Y-%m-%d", + "%Y-%m-%dT%H:%M", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M:%S%z", + ]: + try: + dt = datetime.strptime(val, fmt) + except ValueError: + continue + else: + # if we don't know the timezone, assume the local one + if dt.tzinfo is None: + dt = dt.replace(tzinfo=localtz) + return dt + + # try parsing the debian/changelog datetime format as specified by RFC 2822 + # we cannot use strptime() because that honors the locale and RFC + # 2822 requires that day and month names be the English abbreviations. + try: + dt = email.utils.parsedate_to_datetime(val) + except TypeError: + pass + else: + return dt + + # next, try parsing using dateutil.parser + if HAVE_DATEUTIL: + try: + dt = dateutil.parser.parse(val) + except ValueError: + pass + else: + # if we don't know the timezone, assume the local one + if dt.tzinfo is None: + dt = dt.replace(tzinfo=localtz) + return dt + + # if that didn't work, try freeform formats + if HAVE_PARSEDATETIME: + cal = parsedatetime.Calendar() + dt, ret = cal.parseDT(val) + if ret != 0: + # if we don't know the timezone, assume the local one + if dt.tzinfo is None: + dt = dt.replace(tzinfo=localtz) + return dt + + if not HAVE_DATEUTIL: + logging.info("parsing date failed -- maybe install python3-dateutil") + if not HAVE_PARSEDATETIME: + logging.info("parsing date failed -- maybe install python3-parsedatetime") + + # all failed, we cannot parse this + raise argparse.ArgumentTypeError(f"not a valid datetime: {val}") + + +def qemuarg(val): + defaultmem = "1G" + defaultdisk = "4G" + ret = {"memsize": defaultmem, "disksize": defaultdisk} + for keyval in val.split(","): + # we use startswith() so that "defaults" can also be abbreviated (even + # down to the empty string) + if "defaults".startswith(keyval): + ret["memsize"] = defaultmem + ret["disksize"] = defaultdisk + continue + try: + key, val = keyval.split("=", maxsplit=1) + except ValueError as e: + raise argparse.ArgumentTypeError(f"no key=val pair: {keyval}") from e + if key not in ["memsize", "disksize"]: + raise argparse.ArgumentTypeError(f"unknown key: {key}") + if not re.fullmatch(r"\d+((k|K|M|G|T|P|E|Z|Y)(iB|B)?)?", val): + raise argparse.ArgumentTypeError(f"cannot parse size value: {val}") + ret[key] = val + return ret + + +def scriptarg(val): + if os.path.exists(val) and not os.access(val, os.X_OK): + logging.warning("script %s is a file but not executable", val) + return val + + +def read_pkglist(infile): + result = {} + with open(infile, encoding="utf8") as f: + for line in f: + pkg, version = line.split("\t") + result[pkg] = version.strip() + return result + + +def upgrade_single_package(toupgrade, goodpkgs, badpkgs, good, bad, staticargs): + if toupgrade in goodpkgs: + print( + f"test upgrading {toupgrade} {goodpkgs[toupgrade]}" + f" -> {badpkgs[toupgrade]}..." + ) + else: + print(f"test installing {toupgrade} {badpkgs[toupgrade]}...") + newbadpkgpath = f"./debbisect.{good.strftime('%Y%m%dT%H%M%SZ')}.{toupgrade}.pkglist" + if ( + not staticargs.ignore_cached_results + and os.path.exists(newbadpkgpath) + and os.path.exists(get_log_fname(good, "good", toupgrade)) + ): + print( + f"using cached result (was good)" + f" from {get_log_fname(good, 'good', toupgrade)}" + ) + if toupgrade in goodpkgs: + print(f" upgrading {toupgrade} does not cause the problem") + else: + print(f" installing {toupgrade} does not cause the problem") + return + if ( + not staticargs.ignore_cached_results + and os.path.exists(newbadpkgpath) + and os.path.exists(get_log_fname(good, "bad", toupgrade)) + ): + print( + f"using cached result (was bad)" + f" from {get_log_fname(good, 'bad', toupgrade)}" + ) + print(f" upgrading {toupgrade} triggered the problem") + else: + ret, output = runtest(good, staticargs, toupgrade, bad) + if ret == 0: + write_log_symlink("good", output, good, toupgrade) + if toupgrade in goodpkgs: + print(f" upgrading {toupgrade} does not cause the problem") + else: + print(f" installing {toupgrade} does not cause the problem") + return + write_log_symlink("bad", output, good, toupgrade) + print(f" upgrading {toupgrade} triggered the problem") + # this package introduced the regression check if more than + # just the package in question got upgraded + newbadpkgs = read_pkglist(newbadpkgpath) + # find all packages that are completely new or of a + # different version than those in the last good test + newupgraded = [] + for pkg, version in newbadpkgs.items(): + if pkg not in goodpkgs or version != goodpkgs[pkg]: + newupgraded.append(pkg) + if not newupgraded: + logging.error("no difference -- this should never happen") + sys.exit(1) + elif len(newupgraded) == 1: + # the only upgraded package should be the one that was + # requested to be upgraded + assert newupgraded[0] == toupgrade + else: + print(" additional packages that got upgraded/installed at the same time:") + for newtoupgrade in newupgraded: + if newtoupgrade == toupgrade: + continue + print( + f" {newtoupgrade} {goodpkgs.get(newtoupgrade, '(n.a.)')}" + f" -> {newbadpkgs[newtoupgrade]}" + ) + + +def parseargs(): + progname = sys.argv[0] + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=f"""\ + +Execute a script or a shell snippet for a known good timestamp and a known bad +timestamp and then bisect the timestamps until a timestamp from +snapshot.debian.org is found where the script first fails. Environment +variables are used to tell the script which timestamp to test. See ENVIRONMENT +VARIABLES below. At the end of the execution, the files debbisect.log.good and +debbisect.log.bad are the log files of the last good and last bad run, +respectively. By default, a temporary caching mirror is executed to reduce +bandwidth usage on snapshot.debian.org. If you plan to run debbisect multiple +times on a similar range of timestamps, consider setting a non-temporary cache +directory with the --cache option. + +The program has three basic modes of operation. In the first, the given script +is responsible to set up everything as needed: + + $ {progname} "last week" today script.sh + $ diff -u debbisect.log.good debbisect.log.bad + +If also the --depends option is given, then a chroot of the correct timestamp +will be created each time and the script will receive as first argument the +path to that chroot. Additionally, this mode allows debbisect to figure out the +exact package that was responsible for the failure instead of only presenting +you the last good and first bad timestamp. + +Lastly, you can also provide the --qemu option. In this mode, your test will be +create a qemu virtual machine of the correct timestamp each time. The script +will receive the correct ssh config to log into a host named qemu and execute +arbitrary commands. + +""", + epilog=f"""\ + +*EXAMPLES* + +Just run "do_something" which runs the test and returns a non-zero exit on +failure. + + $ {progname} "last week" today "mmdebstrap --customize-hook\ +='chroot \\"\\$1\\" do_something' unstable - \\$DEBIAN_BISECT_MIRROR >/dev/null" + $ diff -u debbisect.log.good debbisect.log.bad + +Since the command can easily become very long and quoting very involved, lets +instead use a script: + + $ cat << END > script.sh + > #!/bin/sh + > set -exu + > mmdebstrap \\ + > --verbose \\ + > --aptopt='Acquire::Check-Valid-Until "false"' \\ + > --variant=apt \\ + > --include=pkga,pkgb,pkgc \\ + > --customize-hook='chroot "$1" dpkg -l' \\ + > --customize-hook='chroot "$1" do_something' \\ + > unstable \\ + > - \\ + > $DEBIAN_BISECT_MIRROR \\ + > >/dev/null + > END + $ chmod +x script.sh + $ {progname} --verbose --cache=./cache "two years ago" yesterday ./script.sh + $ diff -u debbisect.log.good debbisect.log.bad + $ rm -r ./cache + +This example sets Acquire::Check-Valid-Until to not fail on snapshot timestamps +from "two years ago", uses the "apt" variant (only Essential:yes plus apt), +installs the packages required for the test using --include, runs "dpkg -l" so +that we can see which packages differed in the logs at the end and uses +--cache=cache so that the apt cache does not get discarded at the end and the +command can be re-run without downloading everything from snapshot.debian.org +again. + +If you want to build a source package you can use the script shipped by +devscripts as /usr/share/doc/devscripts/examples/debbisect_buildsrc.sh and +either use it unmodified like this: + + $ DEBIAN_BISECT_SRCPKG=mysrc {progname} "two years ago" yesterday \ + > /usr/share/doc/devscripts/examples/debbisect_buildsrc.sh + +or use the script as a starting point to do your own custom builds. + +Once debbisect has finished bisecting and figured out the last good and the +first bad timestamp, there might be more than one package that differs in +version between these two timestamps. debbisect can figure out which package is +the culprit if you hand it control over installing dependencies for you via the +--depends option. With that option active, the script will not be responsible +to set up a chroot itself but is given the path to an existing chroot as the +first argument. Here is a real example that verifies the package responsible +for Debian bug #912935: + + $ {progname} --depends=botch "2018-11-17" "2018-11-22" \ +'chroot "$1" botch-dose2html --packages=/dev/null --help' + [...] + test upgrading python3-minimal 3.6.7-1 -> 3.7.1-2... + upgrading python3-minimal triggered the problem + +If you want to run above test under qemu, then you would run: + + $ {progname} --depends=botch --qemu=defaults "2018-11-17" "2018-11-22" \ +'ssh -F "$1" qemu botch-dose2html --packages=/dev/null --help' + +In the last two examples we omitted the --cache argument for brevity. But +please make use of it to reduce the load on snapshot.debian.org. + +*TIMESTAMPS* + +Valid good and bad timestamp formats are either: + + > the format used by snapshot.debian.org + > ISO 8601 (with or without time, seconds and timezone) + > RFC 2822 (used in debian/changelog) + > all formats understood by the Python dateutil module (if installed) + > all formats understood by the Python parsedatetime module (if installed) + +Without specifying the timezone explicitly, the local offset is used. + +Examples (corresponding to the items in above list, respectively): + + > 20200313T065326Z + > 2020-03-13T06:53:26+00:00 + > Fri, 29 Nov 2019 14:00:08 +0100 + > 5:50 A.M. on June 13, 1990 + > two weeks ago + +The earliest timestamp that works with debbisect should be 2006-08-10. + +*ENVIRONMENT VARIABLES* + +The following environment variables are available to the test script: + +DEBIAN_BISECT_MIRROR Contains the caching mirror address. + +DEBIAN_BISECT_EPOCH Contains an integer representing the unix epoch of the + current timestamp. The value of this variable can + directly be assigned to SOURCE_DATE_EPOCH. + +DEBIAN_BISECT_TIMESTAMP Contains a timestamp in the format used by + snapshot.debian.org. Can also be generated from + DEBIAN_BISECT_EPOCH via: + date --utc --date=@$DEBIAN_BISECT_EPOCH +%Y%m%dT%H%M%SZ + +DEBIAN_BISECT_* All environment variables starting with DEBIAN_BISECT_ + are passed to the test script. + +Written by Johannes Schauer Marin Rodrigues <josch@debian.org> +""", + ) + parser.add_argument( + "-d", + "--debug", + help="Print lots of debugging statements", + action="store_const", + dest="loglevel", + const=logging.DEBUG, + default=logging.WARNING, + ) + parser.add_argument( + "-v", + "--verbose", + help="Be verbose", + action="store_const", + dest="loglevel", + const=logging.INFO, + ) + parser.add_argument( + "--cache", help="cache directory -- by default $TMPDIR is used", type=str + ) + parser.add_argument("--nocache", help="disable cache", action="store_true") + 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( + "--depends", + help="Comma separated list of binary packages the test script " + "requires. With this option, the test script will run inside a " + "chroot with the requested packages installed.", + type=str, + ) + parser.add_argument( + "--qemu", + help="Create qemu virtual machine and pass a ssh config file to the " + "test script. This argument takes a comma-separated series of " + "key=value pairs to specify the virtual machine memory size (via " + "memsize) and the virtual machine disksize (via disksize). Sizes " + "are measured in bytes or with common unit suffixes like M or G. " + "To pick the default values (disksize=4G,memsize=1G) the shorthand " + "'defaults' can be passed.", + type=qemuarg, + ) + parser.add_argument( + "--architecture", + help="Chosen architecture when creating the chroot with --depends or " + "--qemu (default: native architecture)", + default=subprocess.check_output(["dpkg", "--print-architecture"]).rstrip(), + type=str, + ) + parser.add_argument( + "--suite", + help="Chosen suite when creating the chroot with --depends or --qemu " + "(default: unstable)", + default="unstable", + type=str, + ) + parser.add_argument( + "--components", + help="Chosen components (separated by comma or whitespace) when " + "creating the chroot with --depends or --qemu (default: main)", + default="main", + type=str, + ) + parser.add_argument( + "--no-find-exact-package", + help="Normally, when the --depends argument is given so that " + "debbisect takes care of managing dependencies, the precise package " + "that introduced the problem is determined after bisection by " + "installing the packages that differ between the last good and " + "first bad timestamp one by one. This option disables this feature.", + action="store_true", + ) + parser.add_argument( + "--ignore-cached-results", + help="Perform a run for a timestamp even if a log file for it exists " + "in the current directory", + action="store_true", + ) + parser.add_argument( + "good", + type=datetimestr, + help="good timestamp -- see section TIMESTAMPS for valid formats", + ) + parser.add_argument( + "bad", + type=datetimestr, + help="bad timestamp -- see section TIMESTAMPS for valid formats", + ) + parser.add_argument( + "script", + type=scriptarg, + help="test script -- can either be a shell code snippet or an " + "executable script. A non zero exit code indicates failure. " + "When also --depends is used, then the first argument to the " + "script will be the chroot directory. When --qemu is used, then " + "the first argument to the script will an ssh config for a host " + "named qemu.", + ) + return parser.parse_args() + + +def setupcache(cache, port): + if cache: + cachedir = cache + else: + cachedir = tempfile.mkdtemp(prefix="debbisect") + logging.info("using cache directory: %s", cachedir) + os.makedirs(cachedir + "/pool", exist_ok=True) + httpd = socketserver.TCPServer( + # the default address family for socketserver is AF_INET so we + # explicitly bind to ipv4 localhost + ("127.0.0.1", port), + partial(Proxy, directory=cachedir), + # to avoid "Address already in use" when the port is specified + # manually, we set socket.SO_REUSEADDR + # to do so, we must set allow_reuse_address and then bind and + # activate manually + bind_and_activate=False, + ) + # this sets socket.SO_REUSEADDR + httpd.allow_reuse_address = True + httpd.server_bind() + httpd.server_activate() + # 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 + + +def find_exact_package(good, bad, staticargs, depends, no_find_exact_package): + goodpkglist = f"./debbisect.{good.strftime('%Y%m%dT%H%M%SZ')}.pkglist" + if not os.path.exists(goodpkglist): + logging.error("%s doesn't exist", goodpkglist) + sys.exit(1) + badpkglist = f"./debbisect.{bad.strftime('%Y%m%dT%H%M%SZ')}.pkglist" + if not os.path.exists(badpkglist): + logging.error("%s doesn't exist", badpkglist) + sys.exit(1) + + # Create a set of packages for which either the version in the last good + # and first bad run differs or which only exist in the first bad run. + goodpkgs = read_pkglist(goodpkglist) + badpkgs = read_pkglist(badpkglist) + upgraded = set() + for pkg, version in goodpkgs.items(): + if pkg in badpkgs and version != badpkgs[pkg]: + upgraded.add(pkg) + for pkg, version in badpkgs.items(): + if pkg not in goodpkgs or version != goodpkgs[pkg]: + upgraded.add(pkg) + upgraded = list(sorted(upgraded)) + if not upgraded: + logging.error("no difference -- this should never happen") + sys.exit(1) + elif len(upgraded) == 1: + print( + f"only one package differs: {upgraded[0]}" + f" {goodpkgs.get(upgraded[0], '(n.a.)')} -> {badpkgs[upgraded[0]]}" + ) + else: + print( + "the following packages differ between the last good and " + "first bad timestamp:" + ) + for toupgrade in upgraded: + print( + f" {toupgrade} {goodpkgs.get(toupgrade, '(n.a.)')}" + f" -> {badpkgs[toupgrade]}" + ) + + # if debbisect was tasked with handling dependencies itself, try to + # figure out the exact package that introduce the break + if depends and not no_find_exact_package: + for toupgrade in upgraded: + upgrade_single_package( + toupgrade, goodpkgs, badpkgs, good, bad, staticargs + ) + + +def ensure_mmdebstrap_version(reqver: str) -> bool: + try: + version = subprocess.check_output( + ["mmdebstrap", "--version"], stderr=subprocess.DEVNULL + ) + except subprocess.CalledProcessError: + print("running mmdebstrap --version resulted in non-zero exit") + sys.exit(1) + except FileNotFoundError: + print("you need to install mmdebstrap") + sys.exit(1) + + version = version.decode().removeprefix("mmdebstrap ") + + return debian.debian_support.Version(version) >= debian.debian_support.Version( + reqver + ) + + +def main(): + args = parseargs() + + logging.basicConfig(level=args.loglevel) + + good = sanitize_timestamp(args.good) + if good != args.good: + print( + f"good timestamp {format_timestamp(args.good)} was remapped to" + f" snapshot.d.o timestamp {format_timestamp(good)}" + ) + bad = sanitize_timestamp(args.bad) + if bad != args.bad: + print( + f"bad timestamp {format_timestamp(args.bad)} was remapped to" + f" snapshot.d.o timestamp {format_timestamp(bad)}" + ) + + if good > bad: + print("good is later than bad") + sys.exit(1) + + # check if mmdebstrap is installed and at least 1.3.0 + if (args.depends or args.qemu) and not ensure_mmdebstrap_version("1.3.0"): + print("you need at least mmdebstrap version 1.3.0") + sys.exit(1) + + port = None + if not args.nocache: + port, teardown = setupcache(args.cache, args.port) + atexit.register(teardown) + + staticargs = collections.namedtuple( + "args", + [ + "script", + "port", + "depends", + "architecture", + "suite", + "components", + "qemu", + "ignore_cached_results", + ], + ) + for a in staticargs._fields: + setattr(staticargs, a, getattr(args, a)) + staticargs.port = port + if good == bad: + # test only single timestamp + print(f"trying single timestamp {format_timestamp(good)}...") + if not staticargs.ignore_cached_results and os.path.exists( + get_log_fname(good, "good") + ): + print(f"using cached result (was good) from {get_log_fname(good, 'good')}") + ret = 0 + elif not staticargs.ignore_cached_results and os.path.exists( + get_log_fname(good, "bad") + ): + print(f"using cached result (was bad) from {get_log_fname(good, 'bad')}") + ret = 1 + else: + ret, output = runtest(good, staticargs) + if ret == 0: + print("test script output: good") + write_log_symlink("good", output, good) + else: + print("test script output: bad") + write_log_symlink("bad", output, good) + sys.exit(ret) + res = bisect(good, bad, staticargs) + if res is not None: + good, bad = res + print("bisection finished successfully") + print(f" last good timestamp: {format_timestamp(good)}") + print(f" first bad timestamp: {format_timestamp(bad)}") + + find_exact_package( + good, bad, staticargs, args.depends, args.no_find_exact_package + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/debc.1 b/scripts/debc.1 new file mode 100644 index 0000000..b043ee5 --- /dev/null +++ b/scripts/debc.1 @@ -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>. diff --git a/scripts/debc.pl b/scripts/debc.pl new file mode 120000 index 0000000..1a1d45b --- /dev/null +++ b/scripts/debc.pl @@ -0,0 +1 @@ +debi.pl
\ No newline at end of file diff --git a/scripts/debchange.1 b/scripts/debchange.1 new file mode 100644 index 0000000..d1f02de --- /dev/null +++ b/scripts/debchange.1 @@ -0,0 +1,491 @@ +.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 no; 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 +.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) +.SH AUTHOR +The original author was Christoph Lameter <clameter@debian.org>. +Many substantial changes and improvements were made by Julian Gilbey +<jdg@debian.org>. diff --git a/scripts/debchange.bash_completion b/scripts/debchange.bash_completion new file mode 100644 index 0000000..e2f89c9 --- /dev/null +++ b/scripts/debchange.bash_completion @@ -0,0 +1,90 @@ +# /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' + + lts='buster-lts' + + 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 $lts" + + 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 : diff --git a/scripts/debchange.pl b/scripts/debchange.pl new file mode 100755 index 0000000..73501c3 --- /dev/null +++ b/scripts/debchange.pl @@ -0,0 +1,1884 @@ +#!/usr/bin/perl +# vim: set ai shiftwidth=4 tabstop=4 expandtab: + +# debchange: update the debian changelog using your favorite visual editor +# For options, see the usage message below. +# +# When creating a new changelog section, if either of the environment +# variables DEBEMAIL or EMAIL is set, debchange will use this as the +# uploader's email address (with the former taking precedence), and if +# DEBFULLNAME or NAME is set, it will use this as the uploader's full name. +# Otherwise, it will take the standard values for the current user or, +# failing that, just copy the values from the previous changelog entry. +# +# Originally by Christoph Lameter <clameter@debian.org> +# Modified extensively by Julian Gilbey <jdg@debian.org> +# +# Copyright 1999-2005 by 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 5.008; # We're using PerlIO layers +use strict; +use warnings; +use open ':utf8'; # changelogs are written with UTF-8 encoding +use filetest 'access'; # use access rather than stat for -w +# for checking whether user names are valid and making format() behave +use Encode qw/decode_utf8 encode_utf8/; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use File::Copy; +use File::Basename; +use Cwd; +use Dpkg::Vendor qw(get_current_vendor); +use Dpkg::Changelog::Parse qw(changelog_parse); +use Dpkg::Control; +use Dpkg::Version; +use Devscripts::Compression; +use Devscripts::Debbugs; +use POSIX qw(locale_h strftime); + +setlocale(LC_TIME, "C"); # so that strftime is locale independent + +# Predeclare functions +sub fatal($); +my $warnings = 0; + +# And global variables +my $progname = basename($0); +my $modified_conf_msg; +my %env; +my $CHGLINE; # used by the format O section at the end + +my $compression_re = compression_get_file_extension_regex(); + +my $debian_distro_info; + +sub get_debian_distro_info { + return $debian_distro_info if defined $debian_distro_info; + eval { require Debian::DistroInfo; }; + if ($@) { + printf "libdistro-info-perl is not installed, Debian release names " + . "are not known.\n"; + $debian_distro_info = 0; + } else { + $debian_distro_info = DebianDistroInfo->new(); + } + return $debian_distro_info; +} + +my $ubuntu_distro_info; + +sub get_ubuntu_distro_info { + return $ubuntu_distro_info if defined $ubuntu_distro_info; + eval { require Debian::DistroInfo; }; + if ($@) { + printf "libdistro-info-perl is not installed, Ubuntu release names " + . "are not known.\n"; + $ubuntu_distro_info = 0; + } else { + $ubuntu_distro_info = UbuntuDistroInfo->new(); + } + return $ubuntu_distro_info; +} + +sub get_ubuntu_devel_distro { + my $ubu_info = get_ubuntu_distro_info(); + if ($ubu_info == 0 or !$ubu_info->devel()) { + warn "$progname warning: Unable to determine the current Ubuntu " + . "development release. Using UNRELEASED instead.\n"; + return 'UNRELEASED'; + } else { + return $ubu_info->devel(); + } +} + +sub usage () { + print <<"EOF"; +Usage: $progname [options] [changelog entry] +Options: + -a, --append + Append a new entry to the current changelog + -i, --increment + Increase the Debian release number, adding a new changelog entry + -v <version>, --newversion=<version> + Add a new changelog entry with version number specified + -e, --edit + Don't change version number or add a new changelog entry, just + opens an editor + -r, --release + Update the changelog timestamp. If the distribution is set to + "UNRELEASED", change it to unstable (or another distribution as + specified by --distribution, or the name of the current development + release when run under Ubuntu). + --force-save-on-release + When --release is used and an editor opened to allow inspection + of the changelog, require the user to save the changelog their + editor opened. Otherwise, the original changelog will not be + modified. (default) + --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. $progname --release "" + The entry will not be added to the changelog but its presence will + suppress the editor + --create + Create a new changelog (default) or NEWS file (with --news) and + open for editing + --empty + When creating a new changelog, don't add any changes to it + (i.e. only include the header and trailer lines) + --package <package> + Specify the package name when using --create (optional) + --auto-nmu + Attempt to intelligently determine whether a change to the + changelog represents an NMU (default) + --no-auto-nmu + Do not do so + -n, --nmu + Increment the Debian release number for a non-maintainer upload + --bin-nmu + Increment the Debian release number for a binary non-maintainer upload + -q, --qa + Increment the Debian release number for a Debian QA Team upload + -R, --rebuild + Increment the Debian release number for a no-change rebuild + -s, --security + Increment the Debian release number for a Debian Security Team upload + --lts + Increment the Debian release number for a LTS Security Team upload + --team + Increment the Debian release number for a team upload + -U, --upstream + Increment the Debian release number without any appended derivative + distribution name + --bpo + Increment the Debian release number for a backports upload + to "bookworm-backports" + --stable + Increment the Debian release number for a stable upload. + -l, --local <suffix> + Add a suffix to the Debian version number for a local build + -b, --force-bad-version + Force a version to be less than the current one (e.g., when + backporting) + --allow-lower-version <pattern> + Allow a version to be less than the current one (e.g., when + backporting) if it matches the specified pattern + --force-distribution + Force the provided distribution to be used, even if it doesn't match + the list of known distributions + --closes nnnnn[,nnnnn,...] + Add entries for closing these bug numbers, + getting bug titles from the BTS (bug-tracking system, bugs.debian.org) + --[no]query + [Don\'t] try contacting the BTS to get bug titles (default: do query) + -d, --fromdirname + Add a new changelog entry with version taken from the directory name + -p, --preserve + Preserve the directory name + --no-preserve + Do not preserve the directory name (default) + --vendor <vendor> + Override the distributor ID from dpkg-vendor. + -D, --distribution <dist> + Use the specified distribution in the changelog entry being edited + -u, --urgency <urgency> + Use the specified urgency in the changelog entry being edited + -c, --changelog <changelog> + Specify the name of the changelog to use in place of debian/changelog + No directory traversal or checking is performed in this case. + --news <newsfile> + Specify that the newsfile (default debian/NEWS) is to be edited + --[no]multimaint + When appending an entry to a changelog section (-a), [do not] + indicate if multiple maintainers are now involved (default: do so) + --[no]multimaint-merge + When appending an entry to a changelog section, [do not] merge the + entry into an existing changelog section for the current author. + (default: do not) + -m, --maintmaint + Don\'t change (maintain) the maintainer details in the changelog entry + -M, --controlmaint + Use maintainer name and email from the debian/control Maintainer field + -t, --mainttrailer + Don\'t change (maintain) the trailer line in the changelog entry; i.e. + maintain the maintainer and date/time details + --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(-.+)?') + --no-conf, --noconf + Don\'t read devscripts config files; must be the first option given + --release-heuristic log|changelog + Select heuristic used to determine if a package has been released. + (default: changelog) + --help, -h + Display this help message and exit + --version + Display version information + At most one of -a, -i, -e, -r, -v, -d, -n, --bin-nmu, -q, --qa, -R, -s, + --lts, --team, --bpo, --stable, -l (or their long equivalents) may be used. + With no options, one of -i or -a is chosen by looking at the release + specified in the changelog. + +Default settings modified by devscripts configuration files: +$modified_conf_msg +EOF +} + +sub version () { + print <<"EOF"; +This is $progname, from the Debian devscripts package, version 2.17.10 +This code is copyright 1999-2003 by Julian Gilbey, all rights reserved. +Based on 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. +EOF +} + +# Start by setting default values +my $check_dirname_level = 1; +my $check_dirname_regex = 'PACKAGE(-.+)?'; +my $opt_p = 0; +my $opt_query = 1; +my $opt_release_heuristic = 'changelog'; +my $opt_release_heuristic_re = '^(changelog|log)$'; +my $opt_multimaint = 1; +my $opt_multimaint_merge = 0; +my $opt_tz = undef; +my $opt_t = ''; +my $opt_allow_lower = ''; +my $opt_auto_nmu = 1; +my $opt_force_save_on_release = 1; +my $opt_vendor = undef; + +# 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 = ( + 'DEBCHANGE_PRESERVE' => 'no', + 'DEBCHANGE_QUERY_BTS' => 'yes', + 'DEVSCRIPTS_CHECK_DIRNAME_LEVEL' => 1, + 'DEVSCRIPTS_CHECK_DIRNAME_REGEX' => 'PACKAGE(-.+)?', + 'DEBCHANGE_RELEASE_HEURISTIC' => 'changelog', + 'DEBCHANGE_MULTIMAINT' => 'yes', + 'DEBCHANGE_TZ' => $ENV{TZ}, # undef if TZ unset + 'DEBCHANGE_MULTIMAINT_MERGE' => 'no', + 'DEBCHANGE_MAINTTRAILER' => '', + 'DEBCHANGE_LOWER_VERSION_PATTERN' => '', + 'DEBCHANGE_AUTO_NMU' => 'yes', + 'DEBCHANGE_FORCE_SAVE_ON_RELEASE' => 'yes', + 'DEBCHANGE_VENDOR' => '', + ); + $config_vars{'DEBCHANGE_TZ'} ||= ''; + 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{'DEBCHANGE_PRESERVE'} =~ /^(yes|no)$/ + or $config_vars{'DEBCHANGE_PRESERVE'} = 'no'; + $config_vars{'DEBCHANGE_QUERY_BTS'} =~ /^(yes|no)$/ + or $config_vars{'DEBCHANGE_QUERY_BTS'} = 'yes'; + $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'} =~ /^[012]$/ + or $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'} = 1; + $config_vars{'DEBCHANGE_RELEASE_HEURISTIC'} =~ $opt_release_heuristic_re + or $config_vars{'DEBCHANGE_RELEASE_HEURISTIC'} = 'changelog'; + $config_vars{'DEBCHANGE_MULTIMAINT'} =~ /^(yes|no)$/ + or $config_vars{'DEBCHANGE_MULTIMAINT'} = 'yes'; + $config_vars{'DEBCHANGE_MULTIMAINT_MERGE'} =~ /^(yes|no)$/ + or $config_vars{'DEBCHANGE_MULTIMAINT_MERGE'} = 'no'; + $config_vars{'DEBCHANGE_AUTO_NMU'} =~ /^(yes|no)$/ + or $config_vars{'DEBCHANGE_AUTO_NMU'} = 'yes'; + $config_vars{'DEBCHANGE_FORCE_SAVE_ON_RELEASE'} =~ /^(yes|no)$/ + or $config_vars{'DEBCHANGE_FORCE_SAVE_ON_RELEASE'} = 'yes'; + + 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_p = $config_vars{'DEBCHANGE_PRESERVE'} eq 'yes' ? 1 : 0; + $opt_query = $config_vars{'DEBCHANGE_QUERY_BTS'} eq 'no' ? 0 : 1; + $check_dirname_level = $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'}; + $check_dirname_regex = $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_REGEX'}; + $opt_release_heuristic = $config_vars{'DEBCHANGE_RELEASE_HEURISTIC'}; + $opt_multimaint = $config_vars{'DEBCHANGE_MULTIMAINT'} eq 'no' ? 0 : 1; + $opt_tz = $config_vars{'DEBCHANGE_TZ'}; + $opt_multimaint_merge + = $config_vars{'DEBCHANGE_MULTIMAINT_MERGE'} eq 'no' ? 0 : 1; + $opt_t = ($config_vars{'DEBCHANGE_MAINTTRAILER'} eq 'no' ? 0 : 1) + if $config_vars{'DEBCHANGE_MAINTTRAILER'}; + $opt_allow_lower = $config_vars{'DEBCHANGE_LOWER_VERSION_PATTERN'}; + $opt_auto_nmu = $config_vars{'DEBCHANGE_AUTO_NMU'} eq 'yes'; + $opt_force_save_on_release + = $config_vars{'DEBCHANGE_FORCE_SAVE_ON_RELEASE'} eq 'yes' ? 1 : 0; + $opt_vendor = $config_vars{'DEBCHANGE_VENDOR'}; +} + +# We use bundling so that the short option behaviour is the same as +# with older debchange versions. +my ($opt_help, $opt_version); +my ( + $opt_i, $opt_a, $opt_e, $opt_r, $opt_v, + $opt_b, $opt_d, $opt_D, $opt_u, $opt_force_dist +); +my ( + $opt_n, $opt_bn, $opt_qa, $opt_R, $opt_s, + $opt_lts, $opt_team, $opt_U, $opt_bpo, $opt_stable, + $opt_l, $opt_c, $opt_m, $opt_M, $opt_create, + $opt_package, @closes +); +my ($opt_news); +my ($opt_noconf, $opt_empty); + +Getopt::Long::Configure('bundling'); +GetOptions( + "help|h" => \$opt_help, + "version" => \$opt_version, + "i|increment" => \$opt_i, + "a|append" => \$opt_a, + "e|edit" => \$opt_e, + "r|release" => \$opt_r, + "create" => \$opt_create, + "package=s" => \$opt_package, + "v|newversion=s" => \$opt_v, + "b|force-bad-version" => \$opt_b, + "allow-lower-version=s" => \$opt_allow_lower, + "force-distribution" => \$opt_force_dist, + "d|fromdirname" => \$opt_d, + "p" => \$opt_p, + "preserve!" => \$opt_p, + "D|distribution=s" => \$opt_D, + "u|urgency=s" => \$opt_u, + "n|nmu" => \$opt_n, + "bin-nmu" => \$opt_bn, + "q|qa" => \$opt_qa, + "R|rebuild" => \$opt_R, + "s|security" => \$opt_s, + "team" => \$opt_team, + "U|upstream" => \$opt_U, + "bpo" => \$opt_bpo, + "lts" => \$opt_lts, + "stable" => \$opt_stable, + "l|local=s" => \$opt_l, + "query!" => \$opt_query, + "closes=s" => \@closes, + "c|changelog=s" => \$opt_c, + "news:s" => \$opt_news, + "multimaint!" => \$opt_multimaint, + "multi-maint!" => \$opt_multimaint, + 'multimaint-merge!' => \$opt_multimaint_merge, + 'multi-maint-merge!' => \$opt_multimaint_merge, + "m|maintmaint" => \$opt_m, + "M|controlmaint" => \$opt_M, + "t|mainttrailer!" => \$opt_t, + "check-dirname-level=s" => \$check_dirname_level, + "check-dirname-regex=s" => \$check_dirname_regex, + "noconf" => \$opt_noconf, + "no-conf" => \$opt_noconf, + "release-heuristic=s" => \$opt_release_heuristic, + "empty" => \$opt_empty, + "auto-nmu!" => \$opt_auto_nmu, + "force-save-on-release!" => \$opt_force_save_on_release, + "vendor=s" => \$opt_vendor, + ) + or die +"Usage: $progname [options] [changelog entry]\nRun $progname --help for more details\n"; + +# So that we can distinguish, if required, between an explicit +# passing of -a / -i and their values being automagically deduced +# later on +my $opt_a_passed = $opt_a || 0; +my $opt_i_passed = $opt_i || 0; +$opt_news = 'debian/NEWS' if defined $opt_news and $opt_news eq ''; + +if ($opt_t eq '' && $opt_release_heuristic eq 'changelog') { + $opt_t = 1; +} + +if ($opt_noconf) { + fatal "--no-conf is only acceptable as the first command-line option!"; +} +if ($opt_help) { usage; exit 0; } +if ($opt_version) { version; exit 0; } + +if ($check_dirname_level !~ /^[012]$/) { + fatal "Unrecognised --check-dirname-level value (allowed are 0,1,2)"; +} +if ($opt_release_heuristic !~ $opt_release_heuristic_re) { + fatal "Allowed values for --release-heuristics are log and changelog."; +} + +# Only allow at most one non-help option +fatal +"Only one of -a, -i, -e, -r, -v, -d, -n/--nmu, --bin-nmu, -q/--qa, -R/--rebuild, -s/--security, --lts, --team, --bpo, --stable, -l/--local is allowed;\ntry $progname --help for more help" + if ($opt_i ? 1 : 0) + + ($opt_a ? 1 : 0) + + ($opt_e ? 1 : 0) + + ($opt_r ? 1 : 0) + + ($opt_v ? 1 : 0) + + ($opt_d ? 1 : 0) + + ($opt_n ? 1 : 0) + + ($opt_bn ? 1 : 0) + + ($opt_qa ? 1 : 0) + + ($opt_R ? 1 : 0) + + ($opt_s ? 1 : 0) + + ($opt_lts ? 1 : 0) + + ($opt_team ? 1 : 0) + + ($opt_bpo ? 1 : 0) + + ($opt_stable ? 1 : 0) + + ($opt_l ? 1 : 0) > 1; + +# FIXME shouldn't this be later so that the user can override the urgency, +# e.g. "-s -ulow" ? +if ($opt_s || $opt_lts) { + $opt_u = "high"; +} + +if (defined $opt_u) { + fatal "Urgency can only be one of: low, medium, high, critical, emergency" + unless $opt_u =~ /^(low|medium|high|critical|emergency)$/; +} + +# See if we're Debian, Ubuntu or someone else, if we can +my $vendor; +if (defined $opt_vendor && $opt_vendor) { + $vendor = $opt_vendor; +} else { + if (defined $opt_D) { + # Try to guess the vendor based on the given distribution name + my $distro = $opt_D; + $distro =~ s/-.*//; + my $deb_info = get_debian_distro_info(); + my $ubu_info = get_ubuntu_distro_info(); + if ($deb_info != 0 and $deb_info->valid($distro)) { + $vendor = 'Debian'; + } elsif ($ubu_info != 0 and $ubu_info->valid($distro)) { + $vendor = 'Ubuntu'; + } + } + if (not defined $vendor) { + # Get the vendor from dpkg-vendor (dpkg-vendor --query Vendor) + $vendor = get_current_vendor(); + } +} +$vendor ||= 'Debian'; +if ($vendor eq 'Ubuntu' + and ($opt_n or $opt_bn or $opt_qa or $opt_bpo or $opt_stable or $opt_lts)) +{ + $vendor = 'Debian'; +} + +# Check the distro name given. +if (defined $opt_D) { + if ($vendor eq 'Debian') { + unless ($opt_D + =~ /^(experimental|unstable|sid|UNRELEASED|((old){0,2}stable|testing|buster|bullseye|bookworm|trixie)(-proposed-updates|-security)?|proposed-updates)$/ + ) { + my $deb_info = get_debian_distro_info(); + my ($oldstable_backports, $stable_backports) = ("", ""); + if ($deb_info == 0) { + warn +"$progname warning: Unable to determine Debian's backport distributions.\n"; + } else { + $stable_backports = $deb_info->stable() . "-backports"; +# Silence any potential warnings $deb_info emits when oldstable is no longer supported + local $SIG{__WARN__} = sub { }; + my $oldstable = $deb_info->old(); + $oldstable_backports = "$oldstable-backports" if $oldstable; + } + if ( $deb_info == 0 + || $opt_D + !~ m/^(\Q$stable_backports\E|\Q$oldstable_backports\E)$/) { + $stable_backports = ", " . $stable_backports + if $stable_backports; + $oldstable_backports = ", " . $oldstable_backports + if $oldstable_backports; + warn "$progname warning: Recognised distributions are: \n" + . "experimental, unstable, testing, stable, oldstable, oldoldstable,\n" + . "{trixie,bookworm,bullseye,buster}-proposed-updates,\n" + . "{testing,stable,oldstable,oldoldstable}-proposed-updates,\n" + . "{trixie,bookworm,bullseye,buster}-security,\n" + . "{testing,stable,oldstable,oldoldstable}}-security$oldstable_backports$stable_backports and UNRELEASED.\n" + . "Using your request anyway.\n"; + $warnings++ if not $opt_force_dist; + } + } + } elsif ($vendor eq 'Ubuntu') { + if ($opt_D eq 'UNRELEASED') { + ; + } else { + my $ubu_release = $opt_D; + $ubu_release =~ s/(-updates|-security|-proposed|-backports)$//; + my $ubu_info = get_ubuntu_distro_info(); + if ($ubu_info == 0) { + warn "$progname warning: Unable to determine if $ubu_release " + . "is a valid Ubuntu release.\n"; + } elsif (!$ubu_info->valid($ubu_release)) { + warn "$progname warning: Recognised distributions are:\n{" + . join(',', $ubu_info->supported()) + . "}{,-updates,-security,-proposed,-backports} and UNRELEASED.\n" + . "Using your request anyway.\n"; + $warnings++ if not $opt_force_dist; + } + } + } else { + # Unknown vendor, skip check + } +} + +fatal +"--closes should not be used with --news; put bug numbers in the changelog not the NEWS file" + if $opt_news && @closes; + +# hm, this can probably be used with more than just -i. +fatal "--package can only be used with --create, --increment and --newversion" + if $opt_package && !($opt_create || $opt_i || $opt_v); + +my $changelog_path = $opt_c || $ENV{'CHANGELOG'} || 'debian/changelog'; +my $real_changelog_path = $changelog_path; +if ($opt_news) { $changelog_path = $opt_news; } +if ($changelog_path ne 'debian/changelog' and not $opt_news) { + $check_dirname_level = 0; +} + +# extra --create checks +fatal "--package cannot be used when creating a NEWS file" + if $opt_package && $opt_news; + +if ($opt_create) { + if ( $opt_a + || $opt_i + || $opt_e + || $opt_r + || $opt_b + || $opt_n + || $opt_bn + || $opt_qa + || $opt_R + || $opt_s + || $opt_lts + || $opt_team + || $opt_bpo + || $opt_stable + || $opt_l + || $opt_allow_lower) { + warn +"$progname warning: ignoring -a/-i/-e/-r/-b/--allow-lower-version/-n/--bin-nmu/-q/--qa/-R/-s/--lts/--team/--bpo/--stable,-l options with --create\n"; + $warnings++; + } + if ($opt_package && $opt_d) { + fatal "Can only use one of --package and -d"; + } +} + +@closes = split(/,/, join(',', @closes)); +map { s/^\#//; } @closes; # remove any leading # from bug numbers + +# We'll process the rest of the command line later. + +# Look for the changelog +my $chdir = 0; +if (!$opt_create) { + if ($changelog_path eq 'debian/changelog' or $opt_news) { + until (-f $changelog_path) { + $chdir = 1; + chdir '..' or fatal "Can't chdir ..: $!"; + if (cwd() eq '/') { + fatal +"Cannot find $changelog_path anywhere!\nAre you in the source code tree?\n(You could use --create if you wish to create this file.)"; + } + } + + # Can't write, so stop now. + if (!-w $changelog_path) { + fatal "$changelog_path is not writable!"; + } + } else { + unless (-f $changelog_path) { + fatal +"Cannot find $changelog_path!\nAre you in the correct directory?\n(You could use --create if you wish to create this file.)"; + } + + # Can't write, so stop now. + if (!-w $changelog_path) { + fatal "$changelog_path is not writable!"; + } + } +} else { # $opt_create + unless (-d dirname $changelog_path) { + fatal "Cannot find " + . (dirname $changelog_path) + . " directory!\nAre you in the correct directory?"; + } + if (-f $changelog_path) { + fatal "File $changelog_path already exists!"; + } + unless (-w dirname $changelog_path) { + fatal "Cannot find " + . (dirname $changelog_path) + . " directory!\nAre you in the correct directory?"; + } + if ($opt_news && !-f 'debian/changelog') { + fatal "I can't create $opt_news without debian/changelog present"; + } +} + +##### + +# Find the current version number etc. +my $changelog; +my $PACKAGE = 'PACKAGE'; +my $VERSION = 'VERSION'; +my $MAINTAINER = 'MAINTAINER'; +my $EMAIL = 'EMAIL'; +my $DISTRIBUTION = 'UNRELEASED'; +# when updating the lines below also update the help text, the manpage and the testcases. +my %dists = (10, 'buster', 11, 'bullseye', 12, 'bookworm', 13, 'trixie'); +my $lts_dist = '10'; +my $latest_dist = '12'; +# dist guessed from backports, SRU, security uploads... +my $guessed_dist = ''; +my $CHANGES = ''; +# Changelog urgency, possibly propagated to NEWS files +my $CL_URGENCY = ''; + +if (!$opt_create || ($opt_create && $opt_news)) { + my $file = $opt_create ? 'debian/changelog' : $changelog_path; + $changelog = changelog_parse(file => $file); + + # Now we've read the changelog, set some variables and then + # let's check the directory name is sensible + fatal "No package name in changelog!" + unless exists $changelog->{Source}; + $PACKAGE = $changelog->{Source}; + fatal "No version number in changelog!" + unless exists $changelog->{Version}; + $VERSION = $changelog->{Version}; + fatal "No maintainer in changelog!" + unless exists $changelog->{Maintainer}; + $changelog->{Maintainer} = decode_utf8($changelog->{Maintainer}); + ($MAINTAINER, $EMAIL) = ($changelog->{Maintainer} =~ /^([^<]*) <(.*)>/); + $MAINTAINER ||= ''; + fatal "No distribution in changelog!" + unless exists $changelog->{Distribution}; + + if ($vendor eq 'Ubuntu') { + # In Ubuntu the development release regularly changes, don't just copy + # the previous name. + $DISTRIBUTION = get_ubuntu_devel_distro(); + } else { + $DISTRIBUTION = $changelog->{Distribution}; + } + fatal "No changes in changelog!" + unless exists $changelog->{Changes}; + + # Find the current package version + if ($opt_news) { + my $found_version = 0; + my $found_urgency = 0; + my $clog = changelog_parse(file => $real_changelog_path); + $VERSION = $clog->{Version}; + $VERSION =~ s/~$//; + + $CL_URGENCY = $clog->{Urgency}; + } + + # 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$PACKAGE\\E/g; + my $gooddir; + if ($re =~ m%/%) { $gooddir = eval "cwd() =~ /^$re\$/;"; } + else { $gooddir = eval "basename(cwd()) =~ /^$re\$/;"; } + + if (!$gooddir) { + my $pwd = cwd(); + fatal <<"EOF"; +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 + } + } +} else { + # we're creating and we don't know much about our package + if ($opt_d) { + my $pwd = basename(cwd()); + # The directory name should be <package>-<version> + my $version_chars = '0-9a-zA-Z+\.\-'; + if ($pwd =~ m/^([a-z0-9][a-z0-9+\-\.]+)-([0-9][$version_chars]*)$/) { + $PACKAGE = $1; + $VERSION = "$2-1"; # introduce a Debian version of -1 + } elsif ($pwd =~ m/^[a-z0-9][a-z0-9+\-\.]+$/) { + $PACKAGE = $pwd; + } else { + # don't know anything + } + } + if ($opt_v) { + $VERSION = $opt_v; + } + if ($opt_D) { + $DISTRIBUTION = $opt_D; + } +} + +if ($opt_package) { + if ($opt_package =~ m/^[a-z0-9][a-z0-9+\-\.]+$/) { + $PACKAGE = $opt_package; + } else { + warn +"$progname warning: illegal package name used with --package: $opt_package\n"; + $warnings++; + } +} + +# Clean up after old versions of debchange +if (-f "debian/RELEASED") { + unlink("debian/RELEASED"); +} + +if (-e "$changelog_path.dch") { + fatal "The backup file $changelog_path.dch already exists --\n" + . "please move it before trying again"; +} + +# Is this a native Debian package, i.e., does it have a - in the +# version number? +(my $EPOCH) = ($VERSION =~ /^(\d+):/); +(my $SVERSION = $VERSION) =~ s/^\d+://; +(my $UVERSION = $SVERSION) =~ s/-[^-]*$//; + +# Check, sanitise and decode these environment variables +check_env_utf8('DEBFULLNAME'); +check_env_utf8('NAME'); +check_env_utf8('DEBEMAIL'); +check_env_utf8('EMAIL'); +check_env_utf8('UBUMAIL'); + +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; + } +} +if (exists $env{'UBUMAIL'} and $env{'UBUMAIL'} =~ /^(.*)\s+<(.*)>$/) { + $env{'DEBFULLNAME'} = $1 unless exists $env{'DEBFULLNAME'}; + $env{'UBUMAIL'} = $2; +} + +# Now use the gleaned values to determine our MAINTAINER and EMAIL values +if (!$opt_m and !$opt_M) { + if (exists $env{'DEBFULLNAME'}) { + $MAINTAINER = $env{'DEBFULLNAME'}; + } elsif (exists $env{'NAME'}) { + $MAINTAINER = $env{'NAME'}; + } else { + my @pw = getpwuid $<; + if ($pw[6]) { + if (my $pw = decode_utf8($pw[6])) { + $pw =~ s/,.*//; + $MAINTAINER = $pw; + } else { + warn +"$progname warning: passwd full name field for uid $<\nis not UTF-8 encoded; ignoring\n"; + $warnings++; + } + } + } + # Otherwise, $MAINTAINER retains its default value of the last + # changelog entry + + # Email is easier + if ($vendor eq 'Ubuntu' and exists $env{'UBUMAIL'}) { + $EMAIL = $env{'UBUMAIL'}; + } elsif (exists $env{'DEBEMAIL'}) { + $EMAIL = $env{'DEBEMAIL'}; + } elsif (exists $env{'EMAIL'}) { + $EMAIL = $env{'EMAIL'}; + } else { + warn +"$progname warning: neither DEBEMAIL nor EMAIL environment variable is set\n"; + $warnings++; + my $addr; + if (open MAILNAME, '/etc/mailname') { + warn +"$progname warning: building email address from username and mailname\n"; + $warnings++; + chomp($addr = <MAILNAME>); + close MAILNAME; + } + if (!$addr) { + warn +"$progname warning: building email address from username and FQDN\n"; + $warnings++; + chomp($addr = `hostname --fqdn 2>/dev/null`); + $addr = undef if $?; + } + if ($addr) { + my $user = getpwuid $<; + if (!$user) { + $addr = undef; + } else { + $addr = "$user\@$addr"; + } + } + $EMAIL = $addr if $addr; + } + # Otherwise, $EMAIL retains its default value of the last changelog entry +} # if (! $opt_m and ! $opt_M) + +if ($opt_M) { + if (-f 'debian/control') { + my $parser = Dpkg::Control->new(type => CTRL_INFO_SRC); + $parser->load('debian/control'); + my $maintainer = decode_utf8($parser->{Maintainer}); + if ($maintainer =~ /^(.*)\s+<(.*)>$/) { + $MAINTAINER = $1; + $EMAIL = $2; + } else { + fatal "$progname: invalid debian/control Maintainer field value\n"; + } + } else { + fatal "Missing file debian/control"; + } +} + +##### + +if ( + $opt_auto_nmu + and !$opt_v + and !$opt_l + and !$opt_s + and !$opt_lts + and !$opt_team + and !$opt_qa + and !$opt_R + and !$opt_bpo + and !$opt_bn + and !$opt_n + and !$opt_c + and !$opt_stable + and !(exists $ENV{'CHANGELOG'} and length $ENV{'CHANGELOG'}) + and !$opt_M + and !$opt_create + and !$opt_a_passed + and !$opt_r + and !$opt_e + and $vendor ne 'Ubuntu' + and $vendor ne 'Tanglu' + and !( + $opt_release_heuristic eq 'changelog' + and $changelog->{Distribution} eq 'UNRELEASED' + and !$opt_i_passed + ) +) { + + if (-f 'debian/control') { + my $parser = Dpkg::Control->new(type => CTRL_INFO_SRC); + $parser->load('debian/control'); + my $uploader = decode_utf8($parser->{Uploaders}) || ''; + $uploader =~ s/^\s+//; + my $maintainer = decode_utf8($parser->{Maintainer}); + my @uploaders = split(/\s*,\s*/, $uploader); + + my $packager = "$MAINTAINER <$EMAIL>"; + + if ( $maintainer !~ m/<packages\@qa\.debian\.org>/ + and !grep { $_ eq $packager } ($maintainer, @uploaders) + and $packager ne $changelog->{Maintainer} + and !$opt_team) { + $opt_n = 1; + $opt_a = 0; + } + } else { + fatal "Missing file debian/control"; + } +} +##### + +# Do we need to generate "closes" entries? + +my @closes_text = (); +my $initial_release = 0; +if (@closes and $opt_query) { # and we have to query the BTS + if (!Devscripts::Debbugs::have_soap) { + warn +"$progname warning: libsoap-lite-perl not installed, so cannot query the bug-tracking system\n"; + $opt_query = 0; + $warnings++; + # This will now go and execute the "if (@closes and ! $opt_query)" code + } else { + my $bugs = Devscripts::Debbugs::select("src:" . $PACKAGE); + my $statuses = Devscripts::Debbugs::status( + map { [bug => $_, indicatesource => 1] } @{$bugs}); + if ($statuses eq "") { + warn "$progname: No bugs found for package $PACKAGE\n"; + } + foreach my $close (@closes) { + if ($statuses and exists $statuses->{$close}) { + my $title = $statuses->{$close}->{subject}; + my $pkg = $statuses->{$close}->{package}; + $title =~ s/^($pkg|$PACKAGE): //; + push @closes_text, +"Fix \"$title\" <explain what you changed and why> (Closes: \#$close)\n"; + } else { # not our package, or wnpp + my $bug = Devscripts::Debbugs::status( + [bug => $close, indicatesource => 1]); + if ($bug eq "") { + warn +"$progname warning: unknown bug \#$close does not belong to $PACKAGE,\n disabling closing changelog entry\n"; + $warnings++; + push @closes_text, + "Closes?? \#$close: UNKNOWN BUG IN WRONG PACKAGE!!\n"; + } else { + my $bugtitle = $bug->{$close}->{subject}; + $bugtitle ||= ''; + my $bugpkg = $bug->{$close}->{package}; + $bugpkg ||= '?'; + my $bugsrcpkg = $bug->{$close}->{source}; + $bugsrcpkg ||= '?'; + if ($bugsrcpkg eq $PACKAGE) { + warn +"$progname warning: bug \#$close appears to be already archived,\n disabling closing changelog entry\n"; + $warnings++; + push @closes_text, +"Closes?? \#$close: ALREADY ARCHIVED? $bugtitle!!\n"; + } elsif ($bugpkg eq 'wnpp') { + if ($bugtitle =~ /(^(O|RFA|ITA): )/) { + push @closes_text, +"New maintainer. (Closes: \#$close: $bugtitle)\n"; + } elsif ($bugtitle =~ /(^(RFP|ITP): )/) { + push @closes_text, +"Initial release. (Closes: \#$close: $bugtitle)\n"; + $initial_release = 1; + } + } else { + warn +"$progname warning: bug \#$close belongs to package $bugpkg (src $bugsrcpkg),\n not to $PACKAGE: disabling closing changelog entry\n"; + $warnings++; + push @closes_text, + "Closes?? \#$close: WRONG PACKAGE!! $bugtitle\n"; + } + } + } + } + } +} + +if (@closes and !$opt_query) { # and we don't have to query the BTS + foreach my $close (@closes) { + unless ($close =~ /^\d{3,}$/) { + warn "$progname warning: Bug number $close is invalid; ignoring\n"; + $warnings++; + next; + } + push @closes_text, "Closes: \#$close: \n"; + } +} + +# Get a possible changelog entry from the command line +my $ARGS = join(' ', @ARGV); +my $TEXT = decode_utf8($ARGS); +my $EMPTY_TEXT = 0; + +if (@ARGV and !$TEXT) { + if ($ARGS) { + warn +"$progname warning: command-line changelog entry not UTF-8 encoded; ignoring\n"; + $TEXT = ''; + } else { + $EMPTY_TEXT = 1; + } +} + +# Get the date +my $DATE; +{ + local $ENV{TZ} = $opt_tz if $opt_tz; + $DATE = strftime "%a, %d %b %Y %T %z", localtime(); +} + +if ($opt_news && !$opt_i && !$opt_a) { + if ($VERSION eq $changelog->{Version} && !$opt_v && !$opt_l) { + $opt_a = 1; + } else { + $opt_i = 1; + } +} + +# Are we going to have to figure things out for ourselves? +if ( !$opt_i + && !$opt_v + && !$opt_d + && !$opt_a + && !$opt_e + && !$opt_r + && !$opt_n + && !$opt_bn + && !$opt_qa + && !$opt_R + && !$opt_s + && !$opt_lts + && !$opt_team + && !$opt_bpo + && !$opt_stable + && !$opt_l + && !$opt_create) { + # Yes, we are + if ($opt_release_heuristic eq 'log') { + my @UPFILES = glob("../$PACKAGE\_$SVERSION\_*.upload"); + if (@UPFILES > 1) { + fatal "Found more than one appropriate .upload file!\n" + . "Please use an explicit -a, -i or -v option instead."; + } elsif (@UPFILES == 0) { + $opt_a = 1; + } else { + open UPFILE, "<${UPFILES[0]}" + or fatal "Couldn't open .upload file for reading: $!\n" + . "Please use an explicit -a, -i or -v option instead."; + while (<UPFILE>) { + if ( +m%^(s|Successfully uploaded) (/.*/)?\Q$PACKAGE\E\_\Q$SVERSION\E\_[\w\-\+]+\.changes % + ) { + $opt_i = 1; + last; + } + } + close UPFILE + or fatal "Problems experienced reading .upload file: $!\n" + . "Please use an explicit -a, -i or -v option instead."; + if (!$opt_i) { + warn +"$progname warning: A successful upload of the current version was not logged\n" + . "in the upload log file; adding log entry to current version.\n"; + $opt_a = 1; + } + } + } elsif ($opt_release_heuristic eq 'changelog') { + if ($changelog->{Distribution} eq 'UNRELEASED') { + $opt_a = 1; + } elsif ($EMPTY_TEXT == 1) { + $opt_a = 1; + } else { + $opt_i = 1; + } + } else { + fatal "Bad release heuristic value"; + } +} + +# Open in anticipation.... +unless ($opt_create) { + open S, $changelog_path + or fatal "Cannot open existing $changelog_path: $!"; + + # Read the first stanza from the changelog file + # We do this directly rather than reusing $changelog->{Changes} + # so that we have the verbatim changes rather than a (albeit very + # slightly) reformatted version. See Debian bug #452806 + + while (<S>) { + last if /^ --/; + + $CHANGES .= $_; + } + + chomp $CHANGES; + + # Reset file pointer + seek(S, 0, 0); +} +open O, ">$changelog_path.dch" + or fatal "Cannot write to temporary file: $!"; +# Turn off form feeds; taken from perlform +select((select(O), $^L = "")[0]); + +# Note that we now have to remove it +my $tmpchk = 1; +my ($NEW_VERSION, $NEW_SVERSION, $NEW_UVERSION); +my $line; +my $optionsok = 0; +my $merge = 0; + +if (( + $opt_i + || $opt_n + || $opt_bn + || $opt_qa + || $opt_R + || $opt_s + || $opt_lts + || $opt_team + || $opt_bpo + || $opt_stable + || $opt_l + || $opt_v + || $opt_d + || ($opt_news && $VERSION ne $changelog->{Version})) + && !$opt_create +) { + + $optionsok = 1; + + # Check that a given explicit version number is sensible. + if ($opt_v || $opt_d) { + if ($opt_v) { + $NEW_VERSION = $opt_v; + } else { + my $pwd = basename(cwd()); + # The directory name should be <package>-<version> + my $version_chars = '0-9a-zA-Z+\.~'; + $version_chars .= ':' if defined $EPOCH; + $version_chars .= '\-' if $UVERSION ne $SVERSION; + if ($pwd =~ m/^\Q$PACKAGE\E-([0-9][$version_chars]*)$/) { + $NEW_VERSION = $1; + if ($NEW_VERSION eq $UVERSION) { + # So it's a Debian-native package + if ($SVERSION eq $UVERSION) { + fatal +"New version taken from directory ($NEW_VERSION) is equal to\n" + . "the current version number ($UVERSION)!"; + } + # So we just increment the Debian revision + warn +"$progname warning: Incrementing Debian revision without altering\nupstream version number.\n"; + $VERSION =~ /^(.*?)([a-yA-Y][a-zA-Z]*|\d*)$/; + my $end = $2; + if ($end eq '') { + fatal +"Cannot determine new Debian revision; please use -v option!"; + } + $end++; + $NEW_VERSION = "$1$end"; + } else { + $NEW_VERSION = "$EPOCH:$NEW_VERSION" if defined $EPOCH; + $NEW_VERSION .= "-1"; + } + } else { + fatal +"The directory name must be <package>-<version> for -d to work!\n" + . "No underscores allowed!"; + } + # Don't try renaming the directory in this case! + $opt_p = 1; + } + + if (version_compare($VERSION, $NEW_VERSION) == 1) { + if ($opt_b + or ($opt_allow_lower and $NEW_VERSION =~ /$opt_allow_lower/)) { + warn +"$progname warning: new version ($NEW_VERSION) is less than\n" + . "the current version number ($VERSION).\n"; + } else { + fatal "New version specified ($NEW_VERSION) is less than\n" + . "the current version number ($VERSION)! Use -b to force."; + } + } + + ($NEW_SVERSION = $NEW_VERSION) =~ s/^\d+://; + ($NEW_UVERSION = $NEW_SVERSION) =~ s/-[^-]*$//; + } + + # We use the following criteria for the version and release number: + # the last component of the version number is used as the + # release number. If this is not a Debian native package, then the + # upstream version number is everything up to the final '-', not + # including epochs. + + if (!$NEW_VERSION) { + if ($VERSION =~ /(.*?)([a-yA-Y][a-zA-Z]*|\d+)([+~])?$/i) { + my $extra = $3 || ''; + my $useextra = 0; + my $end = $2; + my $start = $1; + # If it's not already an NMU make it so + # otherwise we can be safe if we behave like dch -i + + if ( + ($opt_n or $opt_s) + and $vendor ne 'Ubuntu' + and $vendor ne 'Tanglu' + and ( ($VERSION eq $UVERSION and not $start =~ /\+nmu/) + or ($VERSION ne $UVERSION and not $start =~ /\.$/)) + ) { + + if ($VERSION eq $UVERSION) { + # First NMU of a Debian native package + $end .= "+nmu1"; + } else { + $end += 0.1; + } + } elsif ($opt_bn and not $start =~ /\+b/) { + $end .= "+b1"; + } elsif ($opt_qa and $start =~ /(.*?)-(\d+)\.$/) { + # Drop NMU revision when doing a QA upload + my $upstream_version = $1; + my $debian_revision = $2; + $debian_revision++; + $start = "$upstream_version-$debian_revision"; + $end = ""; + } elsif ($opt_R + and $vendor eq 'Ubuntu' + and not $start =~ /build/ + and not $start =~ /ubuntu/) { + $end .= "build1"; + } elsif ($opt_R + and $vendor eq 'Tanglu' + and not "$start$end" =~ /(b\d+)$/ + and not $start =~ /tanglu/) { + $end .= "b1"; + } elsif ($opt_bpo and not $start =~ /~bpo[0-9]+\+$/) { + # If it's not already a backport make it so + # otherwise we can be safe if we behave like dch -i + $end .= "~bpo$latest_dist+1"; + } elsif ($opt_stable and not $start =~ /\+deb\d+u/) { + $end .= "+deb${latest_dist}u1"; + } elsif ($opt_lts and not $start =~ /\+deb\d+u/) { + $end .= "+deb${lts_dist}u1"; + $guessed_dist = $dists{$lts_dist} . '-security'; + } elsif ($opt_l and not $start =~ /\Q$opt_l\E/) { + # If it's not already a local package make it so + # otherwise we can be safe if we behave like dch -i + $end .= $opt_l . "1"; + } elsif (!$opt_news) { + # Don't bump the version of a NEWS file in this case as we're + # using the version from the changelog + if ( ($opt_i or $opt_s) + and $vendor eq 'Ubuntu' + and $start !~ /(ubuntu|~ppa)(\d+\.)*$/ + and not $opt_U) { + + if ($start =~ /build/) { + # Drop buildX suffix in favor of ubuntu1 + $start =~ s/build//; + $end = ""; + } + $end .= "ubuntu1"; + } elsif (($opt_i or $opt_s) + and $vendor eq 'Tanglu' + and $start !~ /(tanglu)(\d+\.)*$/ + and not $opt_U) { + + if ("$start$end" =~ /(b\d+)$/) { + # Drop bX suffix in favor of tanglu1 + $start =~ s/b$//; + $end = ""; + } + $end .= "tanglu1"; + } else { + $end++; + } + + # Attempt to set the distribution for a stable upload correctly + # based on the version of the previous upload + if ($opt_stable || $opt_bpo || $opt_s || $opt_lts) { + my $previous_dist = $start; + $previous_dist =~ s/^.*[+~](?:deb|bpo)(\d+)(?:u\+)$/$1/; + if ( defined $previous_dist + and defined $dists{$previous_dist}) { + if ($opt_s || $opt_lts) { + $guessed_dist + = $dists{$previous_dist} . '-security'; + } elsif ($opt_bpo) { + +$guessed_dist + = $dists{$previous_dist} . '-backports'; + } elsif ($opt_stable) { + $guessed_dist = $dists{$previous_dist}; + } + } elsif ($opt_s) { + $guessed_dist = $dists{$latest_dist} . '-security'; + } elsif ($opt_lts) { + $guessed_dist = $dists{$lts_dist} . '-security'; + } else { + # Fallback to using the previous distribution + $guessed_dist = $changelog->{Distribution}; + } + } + + if ( + !( + $opt_s + or $opt_n + or $vendor eq 'Ubuntu' + or $vendor eq 'Tanglu' + ) + ) { + if ($start =~ /(.*?)-(\d+)\.$/) { + # Drop NMU revision + my $upstream_version = $1; + my $debian_revision = $2; + $debian_revision++; + $start = "$upstream_version-$debian_revision"; + $end = ""; + } + } + + if (!($opt_qa or $opt_bpo or $opt_stable or $opt_l)) { + $useextra = 1; + } + } + $NEW_VERSION = "$start$end"; + if ($useextra) { + $NEW_VERSION .= $extra; + } + ($NEW_SVERSION = $NEW_VERSION) =~ s/^\d+://; + ($NEW_UVERSION = $NEW_SVERSION) =~ s/-[^-]*$//; + } else { + fatal "Error parsing version number: $VERSION"; + } + } + + if ($NEW_VERSION eq $NEW_UVERSION and $VERSION ne $UVERSION) { + warn +"$progname warning: New package version is Debian native whilst previous version was not\n"; + } elsif ($NEW_VERSION ne $NEW_UVERSION and $VERSION eq $UVERSION) { + warn +"$progname warning: Previous package version was Debian native whilst new version is not\n" + unless $opt_n or $opt_s; + } + + if ($opt_bpo) { + $guessed_dist ||= $dists{$latest_dist} . '-backports'; + } + if ($opt_stable) { + $guessed_dist ||= $dists{$latest_dist}; + } + my $distribution + = $opt_D + || $guessed_dist + || ( + ($opt_release_heuristic eq 'changelog') + ? "UNRELEASED" + : $DISTRIBUTION + ); + + my $urgency = $opt_u; + if ($opt_news) { + $urgency ||= $CL_URGENCY; + } + $urgency ||= 'medium'; + + if ( ($opt_v or $opt_i or $opt_l or $opt_d) + and $opt_release_heuristic eq 'changelog' + and $changelog->{Distribution} eq 'UNRELEASED') { + + $merge = 1; + } else { + print O "$PACKAGE ($NEW_VERSION) $distribution; urgency=$urgency"; + print O ", binary-only=yes" if ($opt_bn); + print O "\n\n"; + if ($opt_n && !$opt_news) { + print O " * Non-maintainer upload.\n"; + $line = 1; + } elsif ($opt_bn && !$opt_news) { + my $arch = qx/dpkg-architecture -qDEB_BUILD_ARCH/; + chomp($arch); + print O +" * Binary-only non-maintainer upload for $arch; no source changes.\n"; + $line = 1; + } elsif ($opt_qa && !$opt_news) { + print O " * QA upload.\n"; + $line = 1; + } elsif ($opt_s && !$opt_news) { + if ($vendor eq 'Ubuntu' or $vendor eq 'Tanglu') { + print O " * SECURITY UPDATE:\n"; + print O " * References\n"; + } else { + print O " * Non-maintainer upload by the Security Team.\n"; + } + $line = 1; + } elsif ($opt_lts && !$opt_news) { + print O " * Non-maintainer upload by the LTS Security Team.\n"; + $line = 1; + } elsif ($opt_team && !$opt_news) { + print O " * Team upload.\n"; + $line = 1; + } elsif ($opt_bpo && !$opt_news) { + print O " * Rebuild for $guessed_dist.\n"; + $line = 1; + } + if (@closes_text or $TEXT or $EMPTY_TEXT) { + foreach (@closes_text) { format_line($_, 1); } + if (length $TEXT) { format_line($TEXT, 1); } + } elsif ($opt_news) { + print O " \n"; + } else { + print O " * \n"; + } + $line += 3; + print O "\n -- $MAINTAINER <$EMAIL> $DATE\n\n"; + + # Copy the old changelog file to the new one + local $/ = undef; + print O <S>; + } +} +if (($opt_r || $opt_a || $merge) && !$opt_create) { + # This means we just have to generate a new * entry in changelog + # and if a multi-developer changelog is detected, add developer names. + + $NEW_VERSION = $VERSION unless $NEW_VERSION; + $NEW_SVERSION = $SVERSION unless $NEW_SVERSION; + $NEW_UVERSION = $UVERSION unless $NEW_UVERSION; + + # Read and discard maintainer line, see who made the + # last entry, and determine whether there are existing + # multi-developer changes by the current maintainer. + $line = -1; + my ($lastmaint, $nextmaint, $maintline, $count, $lastheader, $lastdist, + $dist_indicator); + my $savedline = $line; + while (<S>) { + $line++; + # Start of existing changes by the current maintainer + if (/^ \[ \Q$MAINTAINER\E \]$/ && $opt_multimaint_merge) { + # If there's more than one such block, + # we only care about the first + $maintline ||= $line; + } elsif (/^ \[ (.*) \]$/ && defined $maintline) { + # Start of existing changes following those by the current + # maintainer + $nextmaint ||= $1; + } elsif ( +m/^\w[-+0-9a-z.]* \(([^\(\) \t]+)\)((?:\s+[-+0-9a-z.]+)+)\;\s+urgency=(\w+)/i + ) { + if (defined $lastmaint) { + $lastheader = $_; + $lastdist = $2; + $lastdist =~ s/^\s+//; + undef $lastdist if $lastdist eq "UNRELEASED"; + # Revert to our previously saved position + $line = $savedline; + last; + } else { + my $tmpver = $1; + $tmpver =~ s/^\s+//; + if ($tmpver =~ m/~bpo(\d+)\+/ && exists $dists{$1}) { + $dist_indicator = "$dists{$1}-backports"; + } + if ($tmpver =~ m/\+deb(\d+)u/ && exists $dists{$1}) { + $dist_indicator = "$dists{$1}"; + } + } + } elsif (/ \* (?:Upload to|Rebuild for) (\S+).*$/) { + ($dist_indicator = $1) =~ s/[!:.,;]$//; + chomp $dist_indicator; + } elsif (/^ --\s+([^<]+)\s+/ || /^ --\s+<(.+?)>/) { + $lastmaint = $1; + # Remember where we are so we can skip back afterwards + $savedline = $line; + } + + if (defined $maintline && !defined $nextmaint) { + $maintline++; + } + } + + # Munging of changelog for multimaintainer mode. + my $multimaint = 0; + if (!$opt_news) { + my $lastmultimaint; + + # Parse the changelog for multi-maintainer maintainer lines of + # the form [ Full Name ] and record the last of these. + while ($CHANGES =~ /.*\n^\s+\[\s+([^\]]+)\s+]\s*$/mg) { + $lastmultimaint = $1; + } + + if (( + !defined $lastmultimaint + && defined $lastmaint + && $lastmaint ne $MAINTAINER + && $opt_multimaint + ) + || (defined $lastmultimaint && $lastmultimaint ne $MAINTAINER) + || (defined $nextmaint) + ) { + $multimaint = 1; + + if (!$lastmultimaint) { + # Add a multi-maintainer header to the top of the existing + # changelog. + my $newchanges = ''; + $CHANGES =~ s/^( .+)$/ [ $lastmaint ]\n$1/m; + } + } + } + + # based on /usr/lib/dpkg/parsechangelog/debian + if ($CHANGES + =~ m/^\w[-+0-9a-z.]* \([^\(\) \t]+\)((?:\s+[-+0-9a-z.]+)+)\;\s+urgency=(\w+)/i + ) { + my $distribution = $1; + my $urgency = $2; + if ($opt_news) { + $urgency = $CL_URGENCY; + } + $distribution =~ s/^\s+//; + if ($opt_r) { + # Change the distribution from UNRELEASED for release + if ($distribution eq "UNRELEASED") { + if ($dist_indicator and not $opt_D) { + $distribution = $dist_indicator; + } elsif ($vendor eq 'Ubuntu') { + if ($opt_D) { + $distribution = $opt_D; + } else { + $distribution = get_ubuntu_devel_distro(); + } + } else { + $distribution = $opt_D || $lastdist || "unstable"; + } + } elsif ($opt_D) { + warn +"$progname warning: ignoring distribution passed to --release as changelog has already been released\n"; + } + # Set the start-line to 1, as we don't know what they want to edit + $line = 1; + } else { + $distribution = $opt_D if $opt_D; + } + $urgency = $opt_u if $opt_u; + $CHANGES + =~ s/^(\w[-+0-9a-z.]* \([^\(\) \t]+\))(?:\s+[-+0-9a-z.]+)+\;\s+urgency=\w+/$PACKAGE ($NEW_VERSION) $distribution; urgency=$urgency/i; + } else { + warn + "$progname: couldn't parse first changelog line, not touching it\n"; + $warnings++; + } + + if (defined $maintline && defined $nextmaint) { + # Output the lines up to the end of the current maintainer block + $count = 1; + $line = $maintline; + foreach (split /\n/, $CHANGES) { + print O $_ . "\n"; + $count++; + last if $count == $maintline; + } + } else { + # The first lines are as we have already found + print O $CHANGES; + } + + if (!$opt_r) { + # Add a multi-maintainer header... + if ($multimaint + and (@closes_text or $TEXT or $opt_news or !$EMPTY_TEXT)) { + # ...unless there already is one for this maintainer. + if (!defined $maintline) { + print O "\n [ $MAINTAINER ]\n"; + $line += 2; + } + } + + if (@closes_text or $TEXT) { + foreach (@closes_text) { format_line($_, 0); } + if (length $TEXT) { format_line($TEXT, 0); } + } elsif ($opt_news) { + print O "\n \n"; + $line++; + } elsif (!$EMPTY_TEXT) { + print O " * \n"; + } + } + + if (defined $count) { + # Output the remainder of the changes + $count = 1; + foreach (split /\n/, $CHANGES) { + $count++; + next unless $count > $maintline; + print O $_ . "\n"; + } + } + + if ($opt_t && $opt_a) { + print O "\n -- $changelog->{Maintainer} $changelog->{Date}\n"; + } else { + print O "\n -- $MAINTAINER <$EMAIL> $DATE\n"; + } + + if ($lastheader) { + print O "\n$lastheader"; + } + + # Copy the rest of the changelog file to new one + # Slurp the rest.... + local $/ = undef; + print O <S>; +} elsif ($opt_e && !$opt_create) { + # We don't do any fancy stuff with respect to versions or adding + # entries, we just update the timestamp and open the editor + + print O $CHANGES; + + if ($opt_t) { + print O "\n -- $changelog->{Maintainer} $changelog->{Date}\n"; + } else { + print O "\n -- $MAINTAINER <$EMAIL> $DATE\n"; + } + + # Copy the rest of the changelog file to the new one + $line = -1; + while (<S>) { $line++; last if /^ --/; } + # Slurp the rest... + local $/ = undef; + print O <S>; + + # Set the start-line to 0, as we don't know what they want to edit + $line = 0; +} elsif ($opt_create) { + if ( !$initial_release + and !$opt_news + and !$opt_empty + and !$TEXT + and !$EMPTY_TEXT) { + push @closes_text, "Initial release. (Closes: \#XXXXXX)\n"; + } + + my $urgency = $opt_u; + if ($opt_news) { + $urgency ||= $CL_URGENCY; + } + $urgency ||= 'medium'; + print O "$PACKAGE ($VERSION) $DISTRIBUTION; urgency=$urgency\n\n"; + + if (@closes_text or $TEXT) { + foreach (@closes_text) { format_line($_, 1); } + if (length $TEXT) { format_line($TEXT, 1); } + } elsif ($opt_news) { + print O " \n"; + } elsif ($opt_empty) { + # Do nothing, but skip the empty entry + } else { # this can't happen, but anyway... + print O " * \n"; + } + + print O "\n -- $MAINTAINER <$EMAIL> $DATE\n"; + + $line = 1; +} elsif (!$optionsok) { + fatal "Unknown changelog processing command line options - help!"; +} + +if (!$opt_create) { + close S or fatal "Error closing $changelog_path: $!"; +} +close O or fatal "Error closing temporary $changelog_path: $!"; + +if ($warnings) { + if ($warnings > 1) { + warn +"$progname: Did you see those $warnings warnings? Press RETURN to continue...\n"; + } else { + warn +"$progname: Did you see that warning? Press RETURN to continue...\n"; + } + my $garbage = <STDIN>; +} + +# Now Run the Editor; always run if doing "closes" to give a chance to check +if ( (!$TEXT and !$EMPTY_TEXT and !($opt_create and $opt_empty)) + or @closes_text + or ($opt_create and !($PACKAGE ne 'PACKAGE' and $VERSION ne 'VERSION'))) { + + my $mtime = (stat("$changelog_path.dch"))[9]; + defined $mtime + or fatal + "Error getting modification time of temporary $changelog_path: $!"; + $mtime--; + utime $mtime, $mtime, "$changelog_path.dch"; + + system("sensible-editor +$line $changelog_path.dch") == 0 + or fatal "Error editing $changelog_path"; + + my $newmtime = (stat("$changelog_path.dch"))[9]; + defined $newmtime + or fatal + "Error getting modification time of temporary $changelog_path: $!"; + if ( $mtime == $newmtime + && !$opt_create + && (!$opt_r || ($opt_r && $opt_force_save_on_release))) { + + warn "$progname: $changelog_path unmodified; exiting.\n"; + exit 0; + } +} + +copy("$changelog_path.dch", "$changelog_path") + or fatal "Couldn't replace $changelog_path with new version: $!"; + +# Now find out what the new package version number is if we need to +# rename the directory + +if ( (basename(cwd()) =~ m%^\Q$PACKAGE\E-\Q$UVERSION\E$%) + && !$opt_p + && !$opt_create) { + # Find the current version number etc. + my $v; + my $changelog = changelog_parse(); + if (exists $changelog->{Version}) { + $v = Dpkg::Version->new($changelog->{Version}); + } + + fatal "No version number in debian/changelog!" + unless defined($v) + and $v->is_valid(); + + my ($new_version, $new_uversion); + $new_version = $v->as_string(omit_epoch => 1); + $new_uversion = $v->as_string(omit_epoch => 1, omit_revision => 1); + + if ($new_uversion ne $UVERSION) { + # Then we rename the directory + if (move(cwd(), "../$PACKAGE-$new_uversion")) { + warn +"$progname warning: your current directory has been renamed to:\n../$PACKAGE-$new_uversion\n"; + } else { + warn "$progname warning: Couldn't rename directory: $!\n"; + } + if (!$v->is_native()) { + # And check whether a new orig tarball exists + my @origs = glob("../$PACKAGE\_$new_uversion.*"); + my $num_origs = grep { +/^..\/\Q$PACKAGE\E_\Q$new_uversion\E\.orig\.tar\.$compression_re$/ + } @origs; + if ($num_origs == 0) { + warn +"$progname warning: no orig tarball found for the new version.\n"; + } + } + } +} + +exit 0; + +{ + no warnings 'uninitialized'; + # Format for standard Debian changelogs + format CHANGELOG = + * ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + $CHGLINE + ~~ ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + $CHGLINE +. + # Format for NEWS files. + format NEWS = + ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + $CHGLINE +~~^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + $CHGLINE +. +} + +my $linecount = 0; + +sub format_line { + $CHGLINE = shift; + my $newentry = shift; + + # Work around the fact that write() with formats + # seems to assume that characters are single-byte + # See https://rt.perl.org/Public/Bug/Display.html?id=33832 + # and Debian bugs #473769 and #541484 + # This relies on $CHGLINE being a sequence of unicode characters. We can + # compare how many unicode characters we have to how many bytes we have + # when encoding to utf8 and therefore how many spaces we need to pad. + my $count = length(encode_utf8($CHGLINE)) - length($CHGLINE); + $CHGLINE .= " " x $count; + + print O "\n" if $opt_news && !($newentry || $linecount); + $linecount++; + my $f = select(O); + if ($opt_news) { + $~ = 'NEWS'; + } else { + $~ = 'CHANGELOG'; + } + write O; + select $f; +} + +BEGIN { + # Initialise the variable + $tmpchk = 0; +} + +END { + if ($tmpchk) { + unlink "$changelog_path.dch" + or warn "$progname warning: Could not remove $changelog_path.dch\n"; + unlink "$changelog_path.dch~"; # emacs backup file + } +} + +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/; + die $msg; +} + +# 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}); + } + } +} diff --git a/scripts/debcheckout.pl b/scripts/debcheckout.pl new file mode 100755 index 0000000..33520e7 --- /dev/null +++ b/scripts/debcheckout.pl @@ -0,0 +1,1260 @@ +#!/usr/bin/perl +# +# debcheckout: checkout the development repository of a Debian package +# Copyright (C) 2007-2009 Stefano Zacchiroli <zack@debian.org> +# Copyright (C) 2010 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 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/>. +# + +# Created: Tue, 14 Aug 2007 10:20:55 +0200 +# Last-Modified: $Date$ + +=head1 NAME + +debcheckout - checkout the development repository of a Debian package + +=head1 SYNOPSIS + +=over + +=item B<debcheckout> [I<OPTIONS>] I<PACKAGE> [I<DESTDIR>] + +=item B<debcheckout> [I<OPTIONS>] I<REPOSITORY_URL> [I<DESTDIR>] + +=item B<debcheckout> B<--help> + +=back + +=head1 DESCRIPTION + +B<debcheckout> retrieves the information about the Version Control System used +to maintain a given Debian package (the I<PACKAGE> argument), and then checks +out the latest (potentially unreleased) version of the package from its +repository. By default the repository is checked out to the I<PACKAGE> +directory; this can be overridden by providing the I<DESTDIR> argument. + +The information about where the repository is available is expected to be found +in B<Vcs-*> fields available in the source package record. For example, the B<vim> +package exposes such information with a field like S<B<Vcs-Hg: +http://hg.debian.org/hg/pkg-vim/vim>>, you can see it by grepping through +B<apt-cache showsrc vim>. + +If more than one source package record containing B<Vcs-*> fields is available, +B<debcheckout> will select the record with the highest version number. +Alternatively, a particular version may be selected from those available by +specifying the package name as I<PACKAGE>=I<VERSION>. + +If you already know the URL of a given repository you can invoke +B<debcheckout> directly on it, but you will probably need to pass the +appropriate B<-t> flag. That is, some heuristics are in use to guess +the repository type from the URL; if they fail, you might want to +override the guessed type using B<-t>. + +The currently supported version control systems are: Arch (arch), Bazaar (bzr), CVS (cvs), +Darcs (darcs), Git (git), Mercurial (hg) and Subversion (svn). + +=head1 OPTIONS + +B<GENERAL OPTIONS> + +=over + +=item B<-a>, B<--auth> + +Work in authenticated mode; this means that for known repositories (mainly those +hosted on S<I<https://salsa.debian.org>>) URL rewriting is attempted before +checking out, to ensure that the repository can be committed to. For example, +for Git repositories hosted on Salsa this means that +S<I<git@salsa.debian.org:...git>> will be used instead of +S<I<https://salsa.debian.org/...git>>. + +There are built-in rules for salsa.debian.org and github.com. Other hosts +can be configured using B<DEBCHECKOUT_AUTH_URLS>. + +=item B<-d>, B<--details> + +Only print a list of detailed information about the package +repository, without checking it out; the output format is a list of +fields, each field being a pair of TAB-separated field name and field +value. The actual fields depend on the repository type. This action +might require a network connection to the remote repository. + +Also see B<-p>. This option and B<-p> are mutually exclusive. + +=item B<-h>, B<--help> + +Print a detailed help message and exit. + +=item B<-p>, B<--print> + +Only print a summary about package repository information, without +checking it out; the output format is TAB-separated with two fields: +repository type, repository URL. This action works offline, it only +uses "static" information as known by APT's cache. + +Also see B<-d>. This option and B<-d> are mutually exclusive. + +=item B<-P> I<package>, B<--package> I<package> + +When checking out a repository URL, instead of trying to guess the package name +from the URL, use this package name. + +=item B<-t> I<TYPE>, B<--type> I<TYPE> + +Override the repository type (which defaults to some heuristics based +on the URL or, in case of heuristic failure, the fallback "git"); +should be one of the currently supported repository types. + +=item B<-u> I<USERNAME>, B<--user> I<USERNAME> + +Specify the login name to be used in authenticated mode (see B<-a>). This option +implies B<-a>: you don't need to specify both. + +=item B<-f> I<FILE>, B<--file> I<FILE> + +Specify that the named file should be extracted from the repository and placed +in the destination directory. May be used more than once to extract multiple +files. + +=item B<--source=never>|B<auto>|B<download-only>|B<always> + +Some packages only place the F<debian> directory in version control. +B<debcheckout> can retrieve the remaining parts of the source using B<apt-get +source> and move the files into the checkout. + +=over + +=item B<never> + +Only use the repository. + +=item B<auto> (default) + +If the repository only contains the F<debian> directory, retrieve the source +package, unpack it, and also place the F<.orig.tar.gz> file into the current +directory. Else, do nothing. + +=item B<download-only> + +Always retrieve the I<.orig.tar.gz> file, but do not unpack it. + +=item B<always> + +Always retrieve the I<.orig.tar.gz> file, and if the repository only contains the +F<debian> directory, unpack it. + +=back + +=back + +B<VCS-SPECIFIC OPTIONS> + +I<GIT-SPECIFIC OPTIONS> + +=over + +=item B<--git-track> I<BRANCHES> + +Specify a list of remote branches which will be set up for tracking +(as in S<B<git branch --track>>, see B<git-branch>(1)) after the remote +Git repository has been cloned. The list should be given as a +space-separated list of branch names. + +As a shorthand, the string "B<*>" can be given to require tracking of all +remote branches. + +=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 + +=item B<DEBCHECKOUT_AUTH_URLS> + +This variable should be a space separated list of Perl regular +expressions and replacement texts, which must come in pairs: I<REGEXP> +I<TEXT> I<REGEXP> I<TEXT> ... and so on. Each pair denotes a substitution which +is applied to repository URLs if other built-in means of building URLs +for authenticated mode (see B<-a>) have failed. + +References to matching substrings in the replacement texts are +allowed as usual in Perl by the means of B<$1>, B<$2>, ... and so on. + +This setting is used to configure the "authenticated mode" location for +repositories. The Debian repositories on S<salsa.debian.org> are implicitly +defined, as is S<github.com>. + +Here is a sample snippet suitable for the configuration files: + + DEBCHECKOUT_AUTH_URLS=' + ^\w+://(svn\.example\.com)/(.*) svn+ssh://$1/srv/svn/$2 + ^\w+://(git\.example\.com)/(.*) git+ssh://$1/home/git/$2 + ' + +Note that whitespace is not allowed in either regexps or +replacement texts. Also, given that configuration files are sourced by +a shell, you probably want to use single quotes around the value of +this variable. + +=item B<DEBCHECKOUT_SOURCE> + +This variable determines under what scenarios the associated orig.tar.gz for a +package will be downloaded. See the B<--source> option for a description of +the values. + +=item B<DEBCHECKOUT_USER> + +This variable sets the username for authenticated mode. It can be overridden +with the B<--user> option. Setting this variable does not imply the use of +authenticated mode, it still has to be activated with B<--auth>. + +=back + +=head1 SEE ALSO + +B<apt-cache>(8), Section 6.2.5 of the Debian Developer's Reference (for +more information about B<Vcs-*> fields): S<I<https://www.debian.org/doc/developers-reference/best-pkging-practices.html#bpp-vcs>>. + +=head1 AUTHOR + +B<debcheckout> and this manpage have been written by Stefano Zacchiroli +<I<zack@debian.org>>. + +=cut + +use strict; +use warnings; +no if $] >= 5.018, 'warnings', 'experimental::smartmatch'; +use feature 'switch'; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use Pod::Usage; +use File::Basename; +use File::Copy qw/copy/; +use File::Temp qw/tempdir/; +use Cwd; +use Devscripts::Compression; +use Devscripts::Versort; + +my @files = (); # files to checkout + +my $compression_re = compression_get_file_extension_regex(); + +# <snippet from="bts.pl"> +# <!-- TODO we really need to factor out in a Perl module the +# configuration file parsing code --> +my @config_files = ('/etc/devscripts.conf', '~/.devscripts'); +my %config_vars = ( + 'DEBCHECKOUT_AUTH_URLS' => '', + 'DEBCHECKOUT_SOURCE' => 'auto', + 'DEBCHECKOUT_USER' => '', +); +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; +# </snippet> + +my $lwp_broken; +my $ua; + +sub have_lwp() { + return ($lwp_broken ? 0 : 1) if defined $lwp_broken; + eval { + require LWP; + require LWP::UserAgent; + }; + + if ($@) { + if ($@ =~ m%^Can\'t locate LWP%) { + $lwp_broken = "the libwww-perl package is not installed"; + } else { + $lwp_broken = "couldn't load LWP::UserAgent: $@"; + } + } else { + $lwp_broken = ''; + } + return $lwp_broken ? 0 : 1; +} + +sub init_agent { + $ua = new LWP::UserAgent; # we create a global UserAgent object + $ua->agent("LWP::UserAgent/Devscripts"); + $ua->env_proxy; +} + +sub recurs_mkdir { + my ($dir) = @_; + my @temp = split /\//, $dir; + my $createdir = ""; + foreach my $piece (@temp) { + if (!length $createdir and !length $piece) { + $createdir = "/"; + } elsif (length $createdir and $createdir ne "/") { + $createdir .= "/"; + } + $createdir .= "$piece"; + if (!-d $createdir) { + mkdir($createdir) or return 0; + } + } + return 1; +} + +# Find the repository URL (and type) for a given package name, parsing Vcs-* +# fields. Returns (version, type, url, origtgz_name) tuple. +sub find_repo($$) { + my ($pkg, $desired_ver) = @_; + my @repo = ("", 0, "", ""); + my $found = 0; + my ($nonepoch_version, $version) = ("", ""); + my $origtgz_name = ""; + my $type = ""; + my $url = ""; + my @repos = (); + + open(APT, "apt-cache showsrc $pkg |"); + while (my $line = <APT>) { + $found = 1; + chomp($line); + if ($line =~ /^(x-)?vcs-(\w+):\s*(.*)$/i) { + next if lc($2) eq "browser"; + ($type, $url) = (lc($2), $3); + } elsif ($line =~ /^Version:\s*(.*)$/i) { + $version = $1; + ($nonepoch_version = $version) =~ s/^\d+://; + } elsif ($line + =~ /^ [a-f0-9]{32} \d+ (\S+)(?:_\Q$nonepoch_version\E|\.orig)\.tar\.$compression_re$/ + ) { + $origtgz_name = $1; + } elsif ($line =~ /^$/) { + push(@repos, [$version, $type, $url, $origtgz_name]) + if ( $version + and $type + and $url + and ($desired_ver eq "" or $desired_ver eq $version)); + $version = ""; + $type = ""; + $url = ""; + $origtgz_name = ""; + } + } + close(APT); + die "unknown package '$pkg'\n" unless $found; + + if (@repos) { + @repos = Devscripts::Versort::versort(@repos); + @repo = @{ $repos[0] }; + } + return @repo; +} + +# Find the browse URL for a given package name, parsing Vcs-* fields. +sub find_browse($$) { + my ($pkg, $desired_ver) = @_; + my $browse = ""; + my $found = 0; + my $version = ""; + my @browses; + + open(APT, "apt-cache showsrc $pkg |"); + while (my $line = <APT>) { + $found = 1; + chomp($line); + if ($line =~ /^(x-)?vcs-(\w+):\s*(.*)$/i) { + if (lc($2) eq "browser") { + $browse = $3; + } + } elsif ($line =~ /^Version:\s*(.*)$/i) { + $version = $1; + } elsif ($line =~ /^$/) { + push(@browses, [$version, $browse]) + if $version + and $browse + and ($desired_ver eq "" or $desired_ver eq $version); + $version = ""; + $browse = ""; + } + } + close(APT); + die "unknown package '$pkg'\n" unless $found; + if (@browses) { + @browses = Devscripts::Versort::versort(@browses); + $browse = $browses[0][1]; + } + return $browse; +} + +# Patch the cmdline invocation of a VCS to ensure the repository is checkout to +# a given target directory. +sub set_destdir($$@) { + my ($repo_type, $destdir, @cmd) = @_; + $destdir =~ s|^-d\s*||; + + given ($repo_type) { + when ("cvs") { + my $module = pop @cmd; + push @cmd, ("-d", $destdir, $module); + } + when (/^(bzr|darcs|git|hg|svn)$/) { + push @cmd, $destdir; + } + default { + die +"sorry, don't know how to set the destination directory for $repo_type repositories (patches welcome!)\n"; + } + } + return @cmd; +} + +# try patching a repository URL to enable authenticated mode, *relying +# only on user defined rules* +sub user_set_auth($$) { + my ($repo_type, $url) = @_; + my @rules = split ' ', $config_vars{'DEBCHECKOUT_AUTH_URLS'}; + while (my $pat = shift @rules) { # read pairs for s/$pat/$subst/ + my $subst = shift @rules + or die +"Configuration error for DEBCHECKOUT_AUTH_URLS: regexp and replacement texts must come in pairs. See debcheckout(1).\n"; + $url =~ s/$pat/qq("$subst")/ee; # ZACK: my worst Perl line ever + } + return $url; +} + +# Patch a given repository URL to ensure that the checked out out repository +# can be committed to. Only works for well known repositories (mainly Salsa's). +sub set_auth($$$$) { + my ($repo_type, $url, $user, $dont_act) = @_; + + my $old_url = $url; + + $user .= "@" if length $user; + my $user_local = $user; + $user_local =~ s|(.*)(@)|$1|; + my $user_url = $url; + + # other providers + $url =~ s!(?:git|https?)://github\.com/!git\@github.com:!; + + given ($repo_type) { + when ("bzr") { + $url + =~ s[^\w+://(?:(bazaar|code)\.)?(launchpad\.net/.*)][bzr+ssh://${user}bazaar.$2]; + } + when ("git") { + $url =~ s!^https://salsa.debian.org/!git\@salsa.debian.org:!; + $url + =~ s[^\w+://(?:(git|code)\.)?(launchpad\.net/.*)][git+ssh://${user}git.$2]; + } + default { + die +"sorry, don't know how to enable authentication for $repo_type repositories (patches welcome!)\n"; + } + } + if ($url eq $old_url) { # last attempt: try with user-defined rules + $url = user_set_auth($repo_type, $url); + } + die +"can't use authenticated mode on repository '$url' since it is not a known repository (e.g. salsa.debian.org)\n" + if $url eq $old_url; + return $url; +} + +# Hack around specific, known deficiencies in repositories that don't follow +# standard behavior. +sub munge_url($$) { + my ($repo_type, $repo_url) = @_; + + return $repo_url; +} + +# returns an error code after system(). If system() exited normally, this is the +# error code of the child process. If it exited with a signal (if a user hit +# C-c, say) then this returns something <0. In either case, errorcode()==0 means +# "success" +sub errorcode { + my $code = $? >> 8; + if ($code == 0 && $? != 0) { + return -$?; + } + return $code; +} + +# Checkout a given repository in a given destination directory. +sub checkout_repo($$$$) { + my ($repo_type, $repo_url, $destdir, $anon_repo_url) = @_; + my (@cmd, @extracmd); + + given ($repo_type) { + when ("arch") { @cmd = ("tla", "grab", $repo_url); } # XXX ??? + when ("bzr") { @cmd = ("bzr", "branch", $repo_url); } + when ("cvs") { + $repo_url =~ s|^-d\s*||; + my ($root, $module) = split /\s+/, $repo_url; + $module ||= ''; + @cmd = ("cvs", "-d", $root, "checkout", $module); + } + when ("darcs") { @cmd = ("darcs", "get", $repo_url); } + when ("git") { + my $push_url; + + if (defined $anon_repo_url and length $anon_repo_url) { + if ($repo_url =~ m|(.*)\s+-b\s+(.*)|) { + $push_url = $1; + } else { + $push_url = $repo_url; + } + + $repo_url = $anon_repo_url; + } + + if ($repo_url =~ m|(.*)\s+-b\s+(.*)|) { + @cmd = ("git", "clone", $1, "-b", $2); + } else { + @cmd = ("git", "clone", $repo_url); + } + + if ($push_url) { + @extracmd = ('git', 'remote', 'set-url', '--push', 'origin', + $push_url); + } + } + when ("hg") { @cmd = ("hg", "clone", $repo_url); } + when ("svn") { @cmd = ("svn", "co", $repo_url); } + default { die "unsupported version control system '$repo_type'.\n"; } + } + @cmd = set_destdir($repo_type, $destdir, @cmd) if length $destdir; + print "@cmd ...\n"; + system @cmd; + my $rc = errorcode(); + + if ($rc == 0 && @extracmd) { + my $oldcwd = getcwd(); + my $clonedir; + + print "@extracmd ...\n"; + + if (length $destdir) { + $clonedir = $destdir; + } else { + ($clonedir = $repo_url) =~ s|.*/(.*)(.git)?|$1|; + } + + chdir $clonedir; + system @extracmd; + $rc = errorcode(); + chdir($oldcwd); + } + + return $rc; +} + +# Checkout a given set of files from a given repository in a given +# destination directory. +sub checkout_files($$$$) { + my ($repo_type, $repo_url, $destdir, $browse_url) = @_; + my @cmd; + my $tempdir; + + foreach my $file (@files) { + my $fetched = 0; + + # Cheap'n'dirty escaping + # We should possibly depend on URI::Escape, but this should do... + my $escaped_file = $file; + $escaped_file =~ s|\+|%2B|g; + + my $dir; + if (defined $destdir and length $destdir) { + $dir = "$destdir/"; + } else { + $dir = "./"; + } + $dir .= dirname($file); + + if (!recurs_mkdir($dir)) { + print STDERR "Failed to create directory $dir\n"; + return 1; + } + + given ($repo_type) { + when ("arch") { + # If we've already retrieved a copy of the repository, + # reuse it + if (!length($tempdir)) { + if ( + !( + $tempdir = tempdir( + "debcheckoutXXXX", + TMPDIR => 1, + CLEANUP => 1 + )) + ) { + print STDERR + "Failed to create temporary directory . $!\n"; + return 1; + } + + my $oldcwd = getcwd(); + chdir $tempdir; + @cmd = ("tla", "grab", $repo_url); + print "@cmd ...\n"; + my $rc = system(@cmd); + chdir $oldcwd; + return ($rc >> 8) if $rc != 0; + } + + if (!copy("$tempdir/$file", $dir)) { + print STDERR "Failed to copy $file to $dir: $!\n"; + return 1; + } + } + when ("cvs") { + if (!length($tempdir)) { + if ( + !( + $tempdir = tempdir( + "debcheckoutXXXX", + TMPDIR => 1, + CLEANUP => 1 + )) + ) { + print STDERR + "Failed to create temporary directory . $!\n"; + return 1; + } + } + $repo_url =~ s|^-d\s*||; + my ($root, $module) = split /\s+/, $repo_url; + # If an explicit module name isn't present, use the last + # component of the URL + if (!length($module)) { + $module = $repo_url; + $module =~ s%^.*/(.*?)$%$1%; + } + $module .= "/$file"; + $module =~ s%//%/%g; + + my $oldcwd = getcwd(); + chdir $tempdir; + @cmd = ("cvs", "-d", $root, "export", "-r", "HEAD", "-f", + $module); + print "\n@cmd ...\n"; + system @cmd; + if (errorcode() != 0) { + chdir $oldcwd; + return (errorcode()); + } else { + chdir $oldcwd; + if (copy("$tempdir/$module", $dir)) { + print "Copied to $destdir/$file\n"; + } else { + print STDERR "Failed to copy $file to $dir: $!\n"; + return 1; + } + } + } + when (/(svn|bzr)/) { + @cmd = ($repo_type, "cat", "$repo_url/$file"); + print "@cmd > $dir/" . basename($file) . " ... \n"; + if (!open CAT, '-|', @cmd) { + print STDERR "Failed to execute @cmd $!\n"; + return 1; + } + local $/; + my $content = <CAT>; + close CAT; + if (!open OUTPUT, ">", $dir . "/" . basename($file)) { + print STDERR "Failed to create output file " + . basename($file) . " $!\n"; + return 1; + } + print OUTPUT $content; + close OUTPUT; + } + when (/(darcs|hg)/) { + # Subtly different but close enough + if (have_lwp) { + print "Attempting to retrieve $file via HTTP ...\n"; + + my $file_url + = $repo_type eq "darcs" + ? "$repo_url/$escaped_file" + : "$repo_url/raw-file/tip/$file"; + init_agent() unless $ua; + my $request = HTTP::Request->new('GET', "$file_url"); + my $response = $ua->request($request); + if ($response->is_success) { + if (!open OUTPUT, ">", $dir . "/" . basename($file)) { + print STDERR "Failed to create output file " + . basename($file) . " $!\n"; + return 1; + } + print "Writing to $dir/" . basename($file) . " ... \n"; + print OUTPUT $response->content; + close OUTPUT; + $fetched = 1; + } + } + if ($fetched == 0) { + # If we've already retrieved a copy of the repository, + # reuse it + if (!length($tempdir)) { + if ( + !( + $tempdir = tempdir( + "debcheckoutXXXX", + TMPDIR => 1, + CLEANUP => 1 + )) + ) { + print STDERR + "Failed to create temporary directory . $!\n"; + return 1; + } + + # Can't get / clone in to a directory that already exists... + $tempdir .= "/repo"; + if ($repo_type eq "darcs") { + @cmd = ("darcs", "get", $repo_url, $tempdir); + } else { + @cmd = ("hg", "clone", $repo_url, $tempdir); + } + print "@cmd ...\n"; + my $rc = system(@cmd); + return ($rc >> 8) if $rc != 0; + print "\n"; + } + } + if (copy "$tempdir/$file", $dir) { + print "Copied $file to $dir\n"; + } else { + print STDERR "Failed to copy $file to $dir: $!\n"; + return 1; + } + } + when ("git") { + # If there isn't a browse URL (either because the package + # doesn't ship one, or because we were called with a URL, + # try a common pattern for gitweb + if (!length($browse_url)) { + if ($repo_url =~ m%^\w+://([^/]+)/(?:git/)?(.*)$%) { + $browse_url = "http://$1/?p=$2"; + } + } + if (have_lwp and $browse_url =~ /^http/) { + $escaped_file =~ s|/|%2F|g; + + print "Attempting to retrieve $file via HTTP ...\n"; + + init_agent() unless $ua; + my $file_url = "$browse_url;a=blob_plain"; + $file_url .= ";f=$escaped_file;hb=HEAD"; + my $request = HTTP::Request->new('GET', $file_url); + my $response = $ua->request($request); + my $error = 0; + if (!$response->is_success) { + if ($browse_url =~ /\.git$/) { + print "Error retrieving file: " + . $response->status_line . "\n"; + $error = 1; + } else { + $browse_url .= ".git"; + $file_url = "$browse_url;a=blob_plain"; + $file_url .= ";f=$escaped_file;hb=HEAD"; + $request = HTTP::Request->new('GET', $file_url); + $response = $ua->request($request); + if (!$response->is_success) { + print "Error retrieving file: " + . $response->status_line . "\n"; + $error = 1; + } + } + } + if (!$error) { + if (!open OUTPUT, ">", $dir . "/" . basename($file)) { + print STDERR "Failed to create output file " + . basename($file) . " $!\n"; + return 1; + } + print "Writing to $dir/" . basename($file) . " ... \n"; + print OUTPUT $response->content; + close OUTPUT; + $fetched = 1; + } + } + if ($fetched == 0) { + # If we've already retrieved a copy of the repository, + # reuse it + if (!length($tempdir)) { + if ( + !( + $tempdir = tempdir( + "debcheckoutXXXX", + TMPDIR => 1, + CLEANUP => 1 + )) + ) { + print STDERR + "Failed to create temporary directory . $!\n"; + return 1; + } + # Since git won't clone in to a directory that + # already exists... + $tempdir .= "/repo"; + # Can't shallow clone from an http:: URL + $repo_url =~ s/^http/git/; + @cmd = ( + "git", "clone", "--depth", "1", $repo_url, + "$tempdir" + ); + print "@cmd ...\n\n"; + my $rc = system(@cmd); + return ($rc >> 8) if $rc != 0; + print "\n"; + } + + my $oldcwd = getcwd(); + chdir $tempdir; + + @cmd = ($repo_type, "show", "HEAD:$file"); + print "@cmd ... > $dir/" . basename($file) . "\n"; + if (!open CAT, '-|', @cmd) { + print STDERR "Failed to execute @cmd $!\n"; + chdir $oldcwd; + return 1; + } + chdir $oldcwd; + local $/; + my $content = <CAT>; + close CAT; + if (!open OUTPUT, ">", $dir . "/" . basename($file)) { + print STDERR "Failed to create output file " + . basename($file) . " $!\n"; + return 1; + } + print OUTPUT $content; + close OUTPUT; + } + } + default { + die "unsupported version control system '$repo_type'.\n"; + } + } + } + + # If we've got this far, all the files were retrieved successfully + return 0; +} + +# download source package, unpack it, and merge its contents into the checkout +sub unpack_source($$$$$) { + my ($pkg, $version, $destdir, $origtgz_name, $unpack_source) = @_; + + return 1 if ($unpack_source eq 'never'); + return 1 + if (defined $origtgz_name and $origtgz_name eq '') + ; # only really relevant with URL on command line + + $destdir ||= $pkg; + # Apt will auto-resolve binary package names to source package names. We + # need to know the source package name to correctly identify the source + # package artifacts (dsc, orig.tar.*, etc) + (my $srcpkg = $origtgz_name) =~ s/_.*//; + # is this a debian-dir-only repository? + unless (-d $destdir) { + print STDERR +"debcheckout did not create the $destdir directory - this is probably a bug\n"; + return 0; + } + my @repo_files = glob "$destdir/*"; + my $debian_only = 0; + if (@repo_files == 1 and $repo_files[0] eq "$destdir/debian") { + $debian_only = 1; + } + + return 1 if ($unpack_source eq 'auto' and not $debian_only); + if ($unpack_source ne 'download-only' and $debian_only) { + print +"repository only contains the debian directory, using apt-get source\n"; + } + + my $tmpdir = File::Temp->newdir(DIR => "."); + + # unpack + my $oldcwd = getcwd(); + chdir $tmpdir; + my @args = ('source'); + push @args, '--download-only' + if ($unpack_source eq 'download-only' or not $debian_only); + push @args, $version ? "$srcpkg=$version" : $srcpkg; + system('apt-get', @args); + chdir $oldcwd; + + if (errorcode()) { + print STDERR "apt-get source failed\n"; + return 0; + } + + # put source package in place + foreach my $sourcefile (glob "$tmpdir/${srcpkg}_*") { + next unless (-f $sourcefile); # skip directories + my $base = $sourcefile; + $base =~ s!.*/!!; + rename $sourcefile, $base or die "rename $sourcefile $base: $!"; + } + + return 1 if ($unpack_source eq 'download-only' or not $debian_only); + + # figure out which directory was created + my @dirs = glob "$tmpdir/$srcpkg-*/"; + unless (@dirs) { + print STDERR + "apt-get source did not create any $tmpdir/$srcpkg-* directory\n"; + return 0; + } + my $directory = $dirs[0]; + chop $directory; + + # move all files over, except the debian directory + opendir DIR, $directory or die "opendir $directory: $!"; + foreach my $file (readdir DIR) { + if ($file eq 'debian') { + system('rm', '-rf', "$directory/$file"); + } elsif ($file eq '.' or $file eq '..') { + next; + } else { + rename "$directory/$file", "$destdir/$file" + or die "rename $directory/$file $destdir/$file: $!"; + } + } + closedir DIR; + rmdir $directory or die "rmdir $directory: $!"; + + # $tmpdir is automatically removed + return 1; +} + +# Print information about a repository and quit. +sub print_repo($$) { + my ($repo_type, $repo_url) = @_; + + print "$repo_type\t$repo_url\n"; + exit(0); +} + +sub git_ls_remote($$) { + my ($url, $prefix) = @_; + + $url =~ s|\s+-b\s+.*||; + my $cmd = "git ls-remote '$url'"; + $cmd .= " '$prefix/*'" if length $prefix; + open GIT, "$cmd |" or die "can't execute $cmd\n"; + my @refs; + while (my $line = <GIT>) { + chomp $line; + my ($sha1, $name) = split /\s+/, $line; + my $ref = $name; + $ref = substr($ref, length($prefix) + 1) if length $prefix; + push @refs, $ref; + } + close GIT; + return @refs; +} + +# Given a GIT repository URL, extract its topgit info (if any), see +# the "topgit" package for more information +sub tg_info($) { + my ($url) = @_; + + my %info; + $info{'topgit'} = 'no'; + $info{'top-bases'} = ''; + my @bases = git_ls_remote($url, 'refs/top-bases'); + if (@bases) { + $info{'topgit'} = 'yes'; + $info{'top-bases'} = join ' ', @bases; + } + return (\%info); +} + +# Print details about a repository and quit. +sub print_details($$) { + my ($repo_type, $repo_url) = @_; + + print "type\t$repo_type\n"; + print "url\t$repo_url\n"; + if ($repo_type eq "git") { + my $tg_info = tg_info($repo_url); + while (my ($k, $v) = each %$tg_info) { + print "$k\t$v\n"; + } + } + exit(0); +} + +sub guess_repo_type($$) { + my ($repo_url, $default) = @_; + my $repo_type = $default; + if ($repo_url =~ /^(git|svn|bzr)(\+ssh)?:/) { + $repo_type = $1; + } elsif ($repo_url =~ /^https?:\/\/(svn|git|hg|bzr|darcs)\.debian\.org/) { + $repo_type = $1; + } elsif ( + $repo_url =~ m@^https?://anonscm.debian.org/(svn|c?git|hg|bzr|darcs)/@) + { + $repo_type = $1; + $repo_type =~ s/cgit/git/; + } + return $repo_type; +} + +# Does a given string match the lexical rules for package names? +sub is_package($) { + my ($arg) = @_; + + return ($arg =~ /^[a-z0-9.+-]+$/); # lexical rule for package names +} + +sub main() { + my $auth = 0; # authenticated mode + my $destdir = ""; # destination directory + my $pkg = ""; # package name + my $version = ""; # package version + my $origtgz_name + = undef; # orig.tar.gz name (or "" when none; undef means unknown) + my $print_mode = 0; # print only mode + my $details_mode = 0; # details only mode + my $use_package = ''; # use this package instead of guessing from the URL + my $repo_type = "git"; # default repo typo, overridden by '-t' + my $repo_url = ""; # repository URL + my $anon_repo_url; # repository URL (before auth mangling) + my $user = ""; # login name (authenticated mode only) + my $browse_url = ""; # online browsable repository URL + my $git_track = ""; # list of remote GIT branches to --track + my $unpack_source + = $config_vars{DEBCHECKOUT_SOURCE}; # retrieve and unpack orig.tar.gz + GetOptions( + "auth|a" => \$auth, + "help|h" => sub { pod2usage({ -exitval => 0, -verbose => 1 }); }, + "print|p" => \$print_mode, + "details|d" => \$details_mode, + "package|P=s" => \$use_package, + "type|t=s" => \$repo_type, + "user|u=s" => \$user, + "file|f=s" => sub { push(@files, $_[1]); }, + "git-track=s" => \$git_track, + "source=s" => \$unpack_source, + ) or pod2usage({ -exitval => 3 }); + pod2usage({ -exitval => 3 }) if ($#ARGV < 0 or $#ARGV > 1); + pod2usage({ + -exitval => 3, + -message => "-d and -p are mutually exclusive.\n", + }) if ($print_mode and $details_mode); + my $dont_act = 1 if ($print_mode or $details_mode); + pod2usage({ + -exitval => 3, + -message => +"--source argument must be one of never, auto, download-only, and always\n", + }) unless ($unpack_source =~ /^(never|auto|download-only|always)$/); + + # -u|--user implies -a|--auth + $auth = 1 if length $user; + + # set user from the config file to be used with -a|--auth without -u|--user + $user = $config_vars{DEBCHECKOUT_USER} unless $user; + + $destdir = $ARGV[1] if $#ARGV > 0; + ($pkg, $version) = split(/=/, $ARGV[0]); + $version ||= ""; + + if (not is_package($pkg)) { # repo-url passed on the command line + $repo_url = $ARGV[0]; + $repo_type = guess_repo_type($repo_url, $repo_type); + $pkg = ""; + $version = ""; + # when --package is given, use it + if ($use_package) { + $pkg = $use_package; + # else guess package from url + } elsif ($repo_url =~ m!/trunk/([a-z0-9.+-]+)!) + { # svn with {trunk,tags,branches}/$pkg + $pkg = $1; + } elsif ($repo_url =~ m!([a-z0-9.+-]+)/trunk/?!) + { # svn with $pkg/{trunk,tags,branches} + $pkg = $1; + } elsif ($repo_url =~ /([a-z0-9.+-]+)\.git(\s+-b\s+.*)?$/) { # git + $pkg = $1; + } elsif ($repo_url =~ /([a-z0-9.+-]+)$/) { # catch-all + $pkg = $1; + } + $origtgz_name = $pkg + ; # FIXME: this should rather set srcpkg in unpack_source() directly + } else { # package name passed on the command line + ($version, $repo_type, $repo_url, $origtgz_name) + = find_repo($pkg, $version); + unless ($repo_type) { + my $vermsg = ""; + $vermsg = ", version $version" if length $version; + print <<EOF; +No repository found for package $pkg$vermsg. + +A Vcs-* field is missing in its source record. See Debian Developer's +Reference 6.2.5: + `https://www.debian.org/doc/developers-reference/best-pkging-practices.html#bpp-vcs' +If you know that the package is maintained via a version control +system consider asking the maintainer to expose such information. + +Nevertheless, you can get the sources of package $pkg +from the Debian archive executing: + + apt-get source $pkg + +Note however that what you obtain will *not* be a local copy of +some version control system: your changes will not be preserved +and it will not be possible to commit them directly. + +EOF + exit(1); + } + $browse_url = find_browse($pkg, $version) if @files; + } + + $repo_url = munge_url($repo_type, $repo_url); + if ($auth and not @files) { + $anon_repo_url = $repo_url; + $repo_url = set_auth($repo_type, $repo_url, $user, $dont_act); + } + print_repo($repo_type, $repo_url) if $print_mode; # ... then quit + print_details($repo_type, $repo_url) if $details_mode; # ... then quit + if (length $pkg) { + print "declared $repo_type repository at $repo_url\n"; + $destdir = $pkg unless length $destdir; + } + my $rc; + if (@files) { + $rc = checkout_files($repo_type, $repo_url, $destdir, $browse_url); + } else { + $rc = checkout_repo($repo_type, $repo_url, $destdir, $anon_repo_url); + } # XXX: there is no way to know for sure what is the destdir :-( + die "checkout failed (the command above returned a non-zero exit code)\n" + if $rc != 0; + + # post-checkout actions + if ($repo_type eq 'bzr' and $auth) { + if (open B, '>>', "$destdir/.bzr/branch/branch.conf") { + print B "\npush_location = $repo_url"; + close B; + } else { + print STDERR + "failed to open branch.conf to add push_location: $!\n"; + } + } elsif ($repo_type eq 'git') { + my $tg_info = tg_info($repo_url); + my $wcdir = $destdir; + # HACK: if $destdir is unknown, take last URL part and remove /.git$/ + $wcdir = (split m|\.|, (split m|/|, $repo_url)[-1])[0] + unless length $wcdir; + if ($$tg_info{'topgit'} eq 'yes') { + print "TopGit detected, populating top-bases ...\n"; + system("cd $wcdir && tg remote --populate origin"); + $rc = errorcode(); + print STDERR "TopGit population failed\n" if $rc != 0; + } + + if (exists $ENV{'DEBEMAIL'} and $ENV{'DEBEMAIL'} =~ /^(.*)\s+<(.*)>$/) + { + $ENV{'DEBFULLNAME'} = $1 unless exists $ENV{'DEBFULLNAME'}; + $ENV{'DEBEMAIL'} = $2; + } + + system("cd $wcdir && git config user.name \"$ENV{'DEBFULLNAME'}\"") + if (defined($ENV{'DEBFULLNAME'})); + system("cd $wcdir && git config user.email \"$ENV{'DEBEMAIL'}\"") + if (defined($ENV{'DEBEMAIL'})); + if (length $git_track) { + my @heads; + if ($git_track eq '*') { + @heads = git_ls_remote($repo_url, 'refs/heads'); + } else { + @heads = split ' ', $git_track; + } + # Filter out any branches already populated via TopGit + my @tgheads = split ' ', $$tg_info{'top-bases'}; + my $master = 'master'; + if ( + open(HEAD, + "env GIT_DIR=\"$wcdir/.git\" git symbolic-ref HEAD |" + ) + ) { + $master = <HEAD>; + chomp $master; + $master =~ s@refs/heads/@@; + } + close(HEAD); + foreach my $head (@heads) { + next if $head eq $master; + next if grep { $head eq $_ } @tgheads; + my $cmd = "cd $wcdir"; + $cmd .= " && git branch --track $head remotes/origin/$head"; + system($cmd); + } + } + } elsif ($repo_type eq 'hg') { + my $username = ''; + $username .= " $ENV{'DEBFULLNAME'}" if (defined($ENV{'DEBFULLNAME'})); + $username .= " <$ENV{'DEBEMAIL'}>" if (defined($ENV{'DEBEMAIL'})); + if ($username) { + if (open(HGRC, '>>', "$destdir/.hg/hgrc")) { + print HGRC "[ui]\nusername =$username\n"; + close HGRC; + } else { + print STDERR "failed to open hgrc to set username: $!\n"; + } + } + } + die "post-checkout action failed\n" + if $rc != 0; + + if ($unpack_source) { + unless ($pkg) { + print STDERR + "could not determine package name for orig.tar.gz retrieval\n"; + $rc ||= 1; + exit($rc); + } + unpack_source($pkg, $version, $destdir, $origtgz_name, $unpack_source) + or $rc = 1; + } + + exit($rc); +} + +main(); + +# vim:sw=4 diff --git a/scripts/debclean.1 b/scripts/debclean.1 new file mode 100644 index 0000000..68fc913 --- /dev/null +++ b/scripts/debclean.1 @@ -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>. diff --git a/scripts/debclean.sh b/scripts/debclean.sh new file mode 100755 index 0000000..5f25807 --- /dev/null +++ b/scripts/debclean.sh @@ -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 diff --git a/scripts/debcommit.pl b/scripts/debcommit.pl new file mode 100755 index 0000000..444510c --- /dev/null +++ b/scripts/debcommit.pl @@ -0,0 +1,958 @@ +#!/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 +signed using gnupg. 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 signed +using gnupg. Currently this is only supported by git. + +=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_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) + --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 $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_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; + $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, + "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 (-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 (-e ".git") { +# With certain forms of git checkouts, .git can be a file instead of a directory + return "git"; + } + 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); + } + $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 diff --git a/scripts/debdiff-apply b/scripts/debdiff-apply new file mode 100755 index 0000000..aa4d8b8 --- /dev/null +++ b/scripts/debdiff-apply @@ -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:])) diff --git a/scripts/debdiff-apply.1 b/scripts/debdiff-apply.1 new file mode 100644 index 0000000..cae8fbd --- /dev/null +++ b/scripts/debdiff-apply.1 @@ -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) diff --git a/scripts/debdiff.1 b/scripts/debdiff.1 new file mode 100644 index 0000000..b2781a7 --- /dev/null +++ b/scripts/debdiff.1 @@ -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. diff --git a/scripts/debdiff.bash_completion b/scripts/debdiff.bash_completion new file mode 100644 index 0000000..d6f373c --- /dev/null +++ b/scripts/debdiff.bash_completion @@ -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 © 2016–2017 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 : diff --git a/scripts/debdiff.pl b/scripts/debdiff.pl new file mode 100755 index 0000000..4bcffc1 --- /dev/null +++ b/scripts/debdiff.pl @@ -0,0 +1,1239 @@ +#!/usr/bin/perl + +# Original shell script version: +# Copyright 1998,1999 Yann Dirson <dirson@debian.org> +# Perl version: +# Copyright 1999,2000,2001 by 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, version 2 ONLY, +# as published by the Free Software Foundation. +# +# 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. + +use 5.006_000; +use strict; +use warnings; +use Cwd; +use Dpkg::IPC; +use File::Copy qw(cp move); +use File::Basename; +use File::Spec; +use File::Path qw/ rmtree /; +use File::Temp qw/ tempdir tempfile /; +use Devscripts::Compression; +use Devscripts::Versort; + +# Predeclare functions +sub wdiff_control_files($$$$$); +sub process_debc($$); +sub process_debI($); +sub mktmpdirs(); +sub fatal(@); + +my $progname = basename($0); +my $modified_conf_msg; +my $exit_status = 0; +my $dummyname = "---DUMMY---"; + +my $compression_re = compression_get_file_extension_regex(); + +sub usage { + print <<"EOF"; +Usage: $progname [option] + or: $progname [option] ... deb1 deb2 + or: $progname [option] ... changes1 changes2 + or: $progname [option] ... dsc1 dsc2 + or: $progname [option] ... --from deb1a deb1b ... --to deb2a deb2b ... +Valid options are: + --no-conf, --noconf + Don\'t read devscripts config files; + must be the first option given + --help, -h Display this message + --version, -v Display version and copyright info + --move FROM TO, The prefix FROM in first packages has + -m FROM TO been renamed TO in the new packages + only affects comparing binary packages + (multiple permitted) + --move-regex FROM TO, The prefix FROM in first packages has + been renamed TO in the new packages + only affects comparing binary packages + (multiple permitted), using regexp substitution + --dirs, -d Note changes in directories as well as files + --nodirs Do not note changes in directories (default) + --nocontrol Skip comparing control files + --control Do compare control files + --controlfiles FILE,FILE,... + Which control files to compare; default is just + control; could include preinst, etc, config or + ALL to compare all control files present + --wp, --wl, --wt Pass the option -p, -l, -t respectively to wdiff + (only one should be used) + --wdiff-source-control When processing source packages, compare control + files as with --control for binary packages + --no-wdiff-source-control + Do not do so (default) + --show-moved Indicate also all files which have moved + between packages + --noshow-moved Do not also indicate all files which have moved + between packages (default) + --renamed FROM TO The package formerly called FROM has been + renamed TO; only of interest with --show-moved + (multiple permitted) + --quiet, -q Be quiet if no differences were found + --exclude PATTERN Exclude files whose basenames match PATTERN + --ignore-space, -w Ignore whitespace in diffs + --diffstat Include the result of diffstat before the diff + --no-diffstat Do not do so (default) + --auto-ver-sort When comparing source packages, ensure the + comparison is performed in version order + --no-auto-ver-sort Do not do so (default) + --unpack-tarballs Unpack tarballs found in the top level source + directory (default) + --no-unpack-tarballs Do not do so + --apply-patches If either old or new package is in 3.0 (quilt) + format, apply the patch series and remove .pc + before comparison + --no-apply-patches Do not do so (default) + +Default settings modified by devscripts configuration files: +$modified_conf_msg + +Use the diffoscope package for deeper comparisons of .deb files. +EOF +} + +my $version = <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +This code is copyright 1999,2000,2001 by Julian Gilbey <jdg\@debian.org>, +based on original code which is copyright 1998,1999 by +Yann Dirson <dirson\@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 ONLY. +EOF + +# Start by setting default values + +my $debsdir; +my $debsdir_warning; +my $ignore_dirs = 1; +my $compare_control = 1; +my $controlfiles = 'control'; +my $show_moved = 0; +my $wdiff_opt = ''; +my @diff_opts = (); +my $show_diffstat = 0; +my $wdiff_source_control = 0; +my $auto_ver_sort = 0; +my $unpack_tarballs = 1; +my $apply_patches = 0; + +my $quiet = 0; + +# Next, read 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 = ( + 'DEBDIFF_DIRS' => 'no', + 'DEBDIFF_CONTROL' => 'yes', + 'DEBDIFF_CONTROLFILES' => 'control', + 'DEBDIFF_SHOW_MOVED' => 'no', + 'DEBDIFF_WDIFF_OPT' => '', + 'DEBDIFF_SHOW_DIFFSTAT' => 'no', + 'DEBDIFF_WDIFF_SOURCE_CONTROL' => 'no', + 'DEBDIFF_AUTO_VER_SORT' => 'no', + 'DEBDIFF_UNPACK_TARBALLS' => 'yes', + 'DEBDIFF_APPLY_PATCHES' => 'no', + 'DEBRELEASE_DEBS_DIR' => '..', + ); + 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; + + # Check validity + $config_vars{'DEBDIFF_DIRS'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_DIRS'} = 'no'; + $config_vars{'DEBDIFF_CONTROL'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_CONTROL'} = 'yes'; + $config_vars{'DEBDIFF_SHOW_MOVED'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_SHOW_MOVED'} = 'no'; + $config_vars{'DEBDIFF_SHOW_DIFFSTAT'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_SHOW_DIFFSTAT'} = 'no'; + $config_vars{'DEBDIFF_WDIFF_SOURCE_CONTROL'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_WDIFF_SOURCE_CONTROL'} = 'no'; + $config_vars{'DEBDIFF_AUTO_VER_SORT'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_AUTO_VER_SORT'} = 'no'; + $config_vars{'DEBDIFF_UNPACK_TARBALLS'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_UNPACK_TARBALLS'} = 'yes'; + $config_vars{'DEBDIFF_APPLY_PATCHES'} =~ /^(yes|no)$/ + or $config_vars{'DEBDIFF_APPLY_PATCHES'} = 'no'; + # 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'}; + $ignore_dirs = $config_vars{'DEBDIFF_DIRS'} eq 'yes' ? 0 : 1; + $compare_control = $config_vars{'DEBDIFF_CONTROL'} eq 'no' ? 0 : 1; + $controlfiles = $config_vars{'DEBDIFF_CONTROLFILES'}; + $show_moved = $config_vars{'DEBDIFF_SHOW_MOVED'} eq 'yes' ? 1 : 0; + $wdiff_opt = $config_vars{'DEBDIFF_WDIFF_OPT'} =~ /^-([plt])$/ ? $1 : ''; + $show_diffstat = $config_vars{'DEBDIFF_SHOW_DIFFSTAT'} eq 'yes' ? 1 : 0; + $wdiff_source_control + = $config_vars{'DEBDIFF_WDIFF_SOURCE_CONTROL'} eq 'yes' ? 1 : 0; + $auto_ver_sort = $config_vars{'DEBDIFF_AUTO_VER_SORT'} eq 'yes' ? 1 : 0; + $unpack_tarballs + = $config_vars{'DEBDIFF_UNPACK_TARBALLS'} eq 'yes' ? 1 : 0; + $apply_patches = $config_vars{'DEBDIFF_APPLY_PATCHES'} eq 'yes' ? 1 : 0; + +} + +# Are they a pair of debs, changes or dsc files, or a list of debs? +my $type = ''; +my @excludes = (); +my @move = (); +my %renamed = (); +my $opt_debsdir; + +# handle command-line options + +while (@ARGV) { + if ($ARGV[0] =~ /^(--help|-h)$/) { usage(); exit 0; } + if ($ARGV[0] =~ /^(--version|-v)$/) { print $version; exit 0; } + if ($ARGV[0] =~ /^(--move(-regex)?|-m)$/) { + fatal +"Malformed command-line option $ARGV[0]; run $progname --help for more info" + unless @ARGV >= 3; + + my $regex = $ARGV[0] eq '--move-regex' ? 1 : 0; + shift @ARGV; + + # Ensure from and to values all begin with a slash + # dpkg -c produces filenames such as ./usr/lib/filename + my $from = shift; + my $to = shift; + $from =~ s%^\./%/%; + $to =~ s%^\./%/%; + + if ($regex) { + # quote ':' in the from and to patterns; + # used later as a pattern delimiter + $from =~ s/:/\\:/g; + $to =~ s/:/\\:/g; + } + push @move, [$regex, $from, $to]; + } elsif ($ARGV[0] eq '--renamed') { + fatal +"Malformed command-line option $ARGV[0]; run $progname --help for more info" + unless @ARGV >= 3; + shift @ARGV; + + my $from = shift; + my $to = shift; + $renamed{$from} = $to; + } elsif ($ARGV[0] eq '--exclude') { + fatal +"Malformed command-line option $ARGV[0]; run $progname --help for more info" + unless @ARGV >= 2; + shift @ARGV; + + my $exclude = shift; + push @excludes, $exclude; + } elsif ($ARGV[0] =~ s/^--exclude=//) { + my $exclude = shift; + push @excludes, $exclude; + } elsif ($ARGV[0] eq '--controlfiles') { + fatal +"Malformed command-line option $ARGV[0]; run $progname --help for more info" + unless @ARGV >= 2; + shift @ARGV; + + $controlfiles = shift; + } elsif ($ARGV[0] =~ s/^--controlfiles=//) { + $controlfiles = shift; + } elsif ($ARGV[0] eq '--debs-dir') { + fatal +"Malformed command-line option $ARGV[0]; run $progname --help for more info" + unless @ARGV >= 2; + shift @ARGV; + + $opt_debsdir = shift; + } elsif ($ARGV[0] =~ s/^--debs-dir=//) { + $opt_debsdir = shift; + } elsif ($ARGV[0] =~ /^(--dirs|-d)$/) { + $ignore_dirs = 0; + shift; + } elsif ($ARGV[0] eq '--nodirs') { + $ignore_dirs = 1; + shift; + } elsif ($ARGV[0] =~ /^(--quiet|-q)$/) { + $quiet = 1; + shift; + } elsif ($ARGV[0] =~ /^(--show-moved|-s)$/) { + $show_moved = 1; + shift; + } elsif ($ARGV[0] eq '--noshow-moved') { + $show_moved = 0; + shift; + } elsif ($ARGV[0] eq '--nocontrol') { + $compare_control = 0; + shift; + } elsif ($ARGV[0] eq '--control') { + $compare_control = 1; + shift; + } elsif ($ARGV[0] eq '--from') { + $type = 'debs'; + last; + } elsif ($ARGV[0] =~ /^--w([plt])$/) { + $wdiff_opt = "-$1"; + shift; + } elsif ($ARGV[0] =~ /^(--ignore-space|-w)$/) { + push @diff_opts, "-w"; + shift; + } elsif ($ARGV[0] eq '--diffstat') { + $show_diffstat = 1; + shift; + } elsif ($ARGV[0] =~ /^--no-?diffstat$/) { + $show_diffstat = 0; + shift; + } elsif ($ARGV[0] eq '--wdiff-source-control') { + $wdiff_source_control = 1; + shift; + } elsif ($ARGV[0] =~ /^--no-?wdiff-source-control$/) { + $wdiff_source_control = 0; + shift; + } elsif ($ARGV[0] eq '--auto-ver-sort') { + $auto_ver_sort = 1; + shift; + } elsif ($ARGV[0] =~ /^--no-?auto-ver-sort$/) { + $auto_ver_sort = 0; + shift; + } elsif ($ARGV[0] eq '--unpack-tarballs') { + $unpack_tarballs = 1; + shift; + } elsif ($ARGV[0] =~ /^--no-?unpack-tarballs$/) { + $unpack_tarballs = 0; + shift; + } elsif ($ARGV[0] eq '--apply-patches') { + $apply_patches = 1; + shift; + } elsif ($ARGV[0] =~ /^--no-?apply-patches$/) { + $apply_patches = 0; + shift; + } elsif ($ARGV[0] =~ /^--no-?conf$/) { + fatal "--no-conf is only acceptable as the first command-line option!"; + } + + # Not a recognised option + elsif ($ARGV[0] =~ /^-/) { + fatal +"Unrecognised command-line option $ARGV[0]; run $progname --help for more info"; + } else { + # End of command line options + last; + } +} + +for my $exclude (@excludes) { + if ($exclude =~ m{/}) { + print STDERR +"$progname: warning: --exclude patterns are matched against the basename, so --exclude='$exclude' will not exclude anything\n"; + } +} + +my $guessed_version = 0; + +if ($opt_debsdir) { + $opt_debsdir =~ s%^/+%/%; + $opt_debsdir =~ s%(.)/$%$1%; + $debsdir_warning = "--debs-dir directory $opt_debsdir does not exist!"; + $debsdir = $opt_debsdir; +} + +# If no file is given, assume that we are in a source directory +# and try to create a diff with the previous version +if (@ARGV == 0) { + my $namepat = qr/[-+0-9a-z.]/i; + + fatal $debsdir_warning unless -d $debsdir; + + fatal "Can't read file: debian/changelog" unless -r "debian/changelog"; + open CHL, "debian/changelog"; + while (<CHL>) { + if (/^(\w$namepat*)\s\((\d+:)?(.+)\)((\s+$namepat+)+)\;\surgency=.+$/) + { + unshift @ARGV, $debsdir . "/" . $1 . "_" . $3 . ".dsc"; + $guessed_version++; + } + last if $guessed_version > 1; + } + close CHL; +} + +if (!$type) { + # we need 2 deb files or changes files to compare + fatal "Need exactly two deb files or changes files to compare" + unless @ARGV == 2; + + foreach my $i (0, 1) { + fatal "Can't read file: $ARGV[$i]" unless -r $ARGV[$i]; + } + + if ($ARGV[0] =~ /\.deb$/) { $type = 'deb'; } + elsif ($ARGV[0] =~ /\.udeb$/) { $type = 'deb'; } + elsif ($ARGV[0] =~ /\.changes$/) { $type = 'changes'; } + elsif ($ARGV[0] =~ /\.dsc$/) { $type = 'dsc'; } + else { + fatal +"Could not recognise files; the names should end .deb, .udeb, .changes or .dsc"; + } + if ($ARGV[1] !~ /\.$type$/ && ($type ne 'deb' || $ARGV[1] !~ /\.udeb$/)) { + fatal +"The two filenames must have the same suffix, either .deb, .udeb, .changes or .dsc"; + } +} + +# We collect up the individual deb information in the hashes +# %debs1 and %debs2, each key of which is a .deb name and each value is +# a list ref. Note we need to use our, not my, as we will be symbolically +# referencing these variables +my @CommonDebs = (); +my @singledeb; +our ( + %debs1, %debs2, %files1, %files2, @D1, + @D2, $dir1, $dir2, %DebPaths1, %DebPaths2 +); + +if ($type eq 'deb') { + no strict 'refs'; + foreach my $i (1, 2) { + my $deb = shift; + my ($debc, $debI) = ('', ''); + my %dpkg_env = (LC_ALL => 'C'); + eval { + spawn( + exec => ['dpkg-deb', '-c', $deb], + env => \%dpkg_env, + to_string => \$debc, + wait_child => 1 + ); + }; + if ($@) { + fatal "dpkg-deb -c $deb failed!"; + } + + eval { + spawn( + exec => ['dpkg-deb', '-I', $deb], + env => \%dpkg_env, + to_string => \$debI, + wait_child => 1 + ); + }; + if ($@) { + fatal "dpkg-deb -I $deb failed!"; + } + # Store the name for later + $singledeb[$i] = $deb; + # get package name itself + $deb =~ s,.*/,,; + $deb =~ s/_.*//; + @{"D$i"} = @{ process_debc($debc, $i) }; + push @{"D$i"}, @{ process_debI($debI) }; + } +} elsif ($type eq 'changes' or $type eq 'debs') { + # Have to parse .changes files or remaining arguments + my $pwd = cwd; + foreach my $i (1, 2) { + my (@debs) = (); + if ($type eq 'debs') { + if (@ARGV < 2) { + # Oops! There should be at least --from|--to deb ... + fatal +"Missing .deb names or missing --to! (Run debdiff -h for help)\n"; + } + shift; # get rid of --from or --to + while (@ARGV and $ARGV[0] ne '--to') { + push @debs, shift; + } + + # Is there only one .deb listed? + if (@debs == 1) { + $singledeb[$i] = $debs[0]; + } + } else { + my $changes = shift; + open CHANGES, $changes + or fatal "Couldn't open $changes: $!"; + my $infiles = 0; + while (<CHANGES>) { + last if $infiles and /^[^ ]/; + /^Files:/ and $infiles = 1, next; + next unless $infiles; + if (/ (\S*.u?deb)$/) { + my $file = $1; + $file !~ m,[/\x00], + or fatal "File name contains invalid characters: $file"; + push @debs, dirname($changes) . '/' . $file; + } + } + close CHANGES + or fatal "Problem reading $changes: $!"; + + # Is there only one .deb listed? + if (@debs == 1) { + $singledeb[$i] = $debs[0]; + } + } + + foreach my $deb (@debs) { + no strict 'refs'; + fatal "Can't read file: $deb" unless -r $deb; + my ($debc, $debI) = ('', ''); + my %dpkg_env = (LC_ALL => 'C'); + eval { + spawn( + exec => ['dpkg-deb', '-c', $deb], + to_string => \$debc, + env => \%dpkg_env, + wait_child => 1 + ); + }; + if ($@) { + fatal "dpkg-deb -c $deb failed!"; + } + eval { + spawn( + exec => ['dpkg-deb', '-I', $deb], + to_string => \$debI, + env => \%dpkg_env, + wait_child => 1 + ); + }; + if ($@) { + fatal "dpkg-deb -I $deb failed!"; + } + my $debpath = $deb; + # get package name itself + $deb =~ s,.*/,,; + $deb =~ s/_.*//; + $deb = $renamed{$deb} if $i == 1 and exists $renamed{$deb}; + if (exists ${"debs$i"}{$deb}) { + warn +"Same package name appears more than once (possibly due to renaming): $deb\n"; + } else { + ${"debs$i"}{$deb} = 1; + } + ${"DebPaths$i"}{$deb} = $debpath; + foreach my $file (@{ process_debc($debc, $i) }) { + ${"files$i"}{$file} ||= ""; + ${"files$i"}{$file} .= "$deb:"; + } + foreach my $control (@{ process_debI($debI) }) { + ${"files$i"}{$control} ||= ""; + ${"files$i"}{$control} .= "$deb:"; + } + } + no strict 'refs'; + @{"D$i"} = keys %{"files$i"}; + # Go back again + chdir $pwd or fatal "Couldn't chdir $pwd: $!"; + } +} elsif ($type eq 'dsc') { + # Compare source packages + my $pwd = cwd; + + my (@origs, @diffs, @dscs, @dscformats, @versions); + foreach my $i (1, 2) { + my $dsc = shift; + chdir dirname($dsc) + or fatal "Couldn't chdir ", dirname($dsc), ": $!"; + + $dscs[$i] = cwd() . '/' . basename($dsc); + + open DSC, basename($dsc) or fatal "Couldn't open $dsc: $!"; + + my $infiles = 0; + while (<DSC>) { + if (/^Files:/) { + $infiles = 1; + next; + } elsif (/^Format: (.*)$/) { + $dscformats[$i] = $1; + } elsif (/^Version: (.*)$/) { + $versions[$i - 1] = [$1, $i]; + } + next unless $infiles; + last if /^\s*$/; + last if /^[-\w]+:/; # don't expect this, but who knows? + chomp; + + # This had better match + if (/^\s+[0-9a-f]{32}\s+\d+\s+(\S+)$/) { + my $file = $1; + $file !~ m,[/\x00], + or fatal "File name contains invalid characters: $file"; + if ($file =~ /\.diff\.gz$/) { + $diffs[$i] = cwd() . '/' . $file; + } elsif ($file =~ /((?:\.orig)?\.tar\.$compression_re|\.git)$/) + { + $origs[$i] = $file; + } + } else { + warn "Unrecognised file line in .dsc:\n$_\n"; + } + } + + close DSC or fatal "Problem closing $dsc: $!"; + # Go back again + chdir $pwd or fatal "Couldn't chdir $pwd: $!"; + } + + @versions = Devscripts::Versort::versort(@versions); + # If the versions are currently out of order, should we swap them? + if ( $auto_ver_sort + and !$guessed_version + and $versions[0][1] == 1 + and $versions[0][0] ne $versions[1][0]) { + foreach my $var ((\@origs, \@diffs, \@dscs, \@dscformats)) { + my $temp = @{$var}[1]; + @{$var}[1] = @{$var}[2]; + @{$var}[2] = $temp; + } + } + + # Do we have interdiff? + system("command -v interdiff >/dev/null 2>&1"); + my $use_interdiff = ($? == 0) ? 1 : 0; + system("command -v diffstat >/dev/null 2>&1"); + my $have_diffstat = ($? == 0) ? 1 : 0; + system("command -v wdiff >/dev/null 2>&1"); + my $have_wdiff = ($? == 0) ? 1 : 0; + + my ($fh, $filename) = tempfile( + "debdiffXXXXXX", + SUFFIX => ".diff", + DIR => File::Spec->tmpdir, + UNLINK => 1 + ); + + # When wdiffing source control files we always fully extract both source + # packages as it's the easiest way of getting the debian/control file, + # particularly if the orig tar ball contains one which is patched in the + # diffs + if ( $origs[1] eq $origs[2] + and defined $diffs[1] + and defined $diffs[2] + and scalar(@excludes) == 0 + and $use_interdiff + and !$wdiff_source_control) { + # same orig tar ball, interdiff exists and not wdiffing + + my $tmpdir = tempdir(CLEANUP => 1); + eval { + spawn( + exec => ['interdiff', '-z', @diff_opts, $diffs[1], $diffs[2]], + to_file => $filename, + wait_child => 1, + # Make interdiff put its tempfiles in $tmpdir, so they're + # automatically cleaned up + env => { TMPDIR => $tmpdir }); + }; + + # If interdiff fails for some reason, we'll fall back to our manual + # diffing. + unless ($@) { + if ($have_diffstat and $show_diffstat) { + my $header + = "diffstat for " + . basename($diffs[1]) . " " + . basename($diffs[2]) . "\n\n"; + $header =~ s/\.diff\.gz//g; + print $header; + spawn( + exec => ['diffstat', $filename], + wait_child => 1 + ); + print "\n"; + } + + if (-s $filename) { + open(INTERDIFF, '<', $filename); + while (<INTERDIFF>) { + print $_; + } + close INTERDIFF; + + $exit_status = 1; + } + exit $exit_status; + } + } + + # interdiff ran and failed, or any other situation + if (!$use_interdiff) { + warn +"Warning: You do not seem to have interdiff (in the patchutils package)\ninstalled; this program would use it if it were available.\n"; + } + # possibly different orig tarballs, or no interdiff installed, + # or wdiffing debian/control + our ($sdir1, $sdir2); + mktmpdirs(); + + for my $i (1, 2) { + no strict 'refs'; + my @opts = ('-x'); + if ($dscformats[$i] eq '3.0 (quilt)' && !$apply_patches) { + push @opts, '--skip-patches'; + } + my $diri = ${"dir$i"}; + eval { + spawn( + exec => ['dpkg-source', @opts, $dscs[$i]], + to_file => '/dev/null', + chdir => $diri, + wait_child => 1 + ); + }; + if ($@) { + my $dir = dirname $dscs[1] if $i == 2; + $dir = dirname $dscs[2] if $i == 1; + cp "$dir/$origs[$i]", + $diri || fatal "copy $dir/$origs[$i] $diri: $!"; + my $dscx = basename $dscs[$i]; + cp $diffs[$i], $diri || fatal "copy $diffs[$i] $diri: $!"; + cp $dscs[$i], $diri || fatal "copy $dscs[$i] $diri: $!"; + spawn( + exec => ['dpkg-source', @opts, $dscx], + to_file => '/dev/null', + chdir => $diri, + wait_child => 1 + ); + } + opendir DIR, $diri; + while ($_ = readdir(DIR)) { + next if $_ eq '.' || $_ eq '..' || !-d "$diri/$_"; + ${"sdir$i"} = $_; + last; + } + closedir(DIR); + my $sdiri = ${"sdir$i"}; + +# also unpack tarballs found in the top level source directory so we can compare their contents too + next unless $unpack_tarballs; + opendir DIR, $diri . '/' . $sdiri; + + my $tarballs = 1; + while ($_ = readdir(DIR)) { + my $unpacked = "=unpacked-tar" . $tarballs . "="; + my $filename = $_; + if ($filename =~ s/\.tar\.$compression_re$//) { + my $comp = compression_guess_from_filename($_); + $tarballs++; + spawn( + exec => ['tar', "--$comp", '-xf', $_], + to_file => '/dev/null', + wait_child => 1, + chdir => "$diri/$sdiri", + nocheck => 1 + ); + if (-d "$diri/$sdiri/$filename") { + move "$diri/$sdiri/$filename", "$diri/$sdiri/$unpacked"; + } + } + } + closedir(DIR); + if ($dscformats[$i] eq '3.0 (quilt)' && $apply_patches) { + spawn( + exec => ['rm', '-fr', "$diri/$sdiri/.pc"], + wait_child => 1 + ); + } + } + + my @command = ("diff", "-Nru", @diff_opts); + for my $exclude (@excludes) { + push @command, ("--exclude", $exclude); + } + push @command, ("$dir1/$sdir1", "$dir2/$sdir2"); + +# Execute diff and remove the common prefixes $dir1/$dir2, so the patch can be used with -p1, +# as if when interdiff would have been used: + spawn( + exec => \@command, + to_file => $filename, + wait_child => 1, + nocheck => 1 + ); + + if ($have_diffstat and $show_diffstat) { + print "diffstat for $sdir1 $sdir2\n\n"; + spawn( + exec => ['diffstat', $filename], + wait_child => 1 + ); + print "\n"; + } + + if ($have_wdiff and $wdiff_source_control) { + # Abuse global variables slightly to create some temporary directories + my $tempdir1 = $dir1; + my $tempdir2 = $dir2; + mktmpdirs(); + our $wdiffdir1 = $dir1; + our $wdiffdir2 = $dir2; + $dir1 = $tempdir1; + $dir2 = $tempdir2; + our @cf; + + if ($controlfiles eq 'ALL') { + @cf = ('control'); + } else { + @cf = split /,/, $controlfiles; + } + + no strict 'refs'; + for my $i (1, 2) { + foreach my $file (@cf) { + cp ${"dir$i"} . '/' . ${"sdir$i"} . "/debian/$file", + ${"wdiffdir$i"}; + } + } + use strict 'refs'; + + # We don't support "ALL" for source packages as that would + # wdiff debian/* + $exit_status = wdiff_control_files($wdiffdir1, $wdiffdir2, $dummyname, + $controlfiles eq 'ALL' ? 'control' : $controlfiles, $exit_status); + print "\n"; + + # Clean up + rmtree([$wdiffdir1, $wdiffdir2]); + } + + if (!-f $filename) { + fatal "Creation of diff file $filename failed!"; + } elsif (-s $filename) { + open(DIFF, '<', $filename) + or fatal "Opening diff file $filename failed!"; + + while (<DIFF>) { + s/^--- $dir1\//--- /; + s/^\+\+\+ $dir2\//+++ /; + s/^(diff .*) $dir1\/\Q$sdir1\E/$1 $sdir1/; + s/^(diff .*) $dir2\/\Q$sdir2\E/$1 $sdir2/; + print; + } + close DIFF; + + $exit_status = 1; + } + + exit $exit_status; +} else { + fatal "Internal error: \$type = $type unrecognised"; +} + +# Compare +# Start by a piece of common code to set up the @CommonDebs list and the like + +my (@deblosses, @debgains); + +{ + my %debs; + grep $debs{$_}--, keys %debs1; + grep $debs{$_}++, keys %debs2; + + @deblosses = sort grep $debs{$_} < 0, keys %debs; + @debgains = sort grep $debs{$_} > 0, keys %debs; + @CommonDebs = sort grep $debs{$_} == 0, keys %debs; +} + +if ($show_moved and $type ne 'deb') { + if (@debgains) { + my $msg + = "Warning: these package names were in the second list but not in the first:"; + print $msg, "\n", '-' x length $msg, "\n"; + print join("\n", @debgains), "\n\n"; + } + + if (@deblosses) { + print "\n" if @debgains; + my $msg + = "Warning: these package names were in the first list but not in the second:"; + print $msg, "\n", '-' x length $msg, "\n"; + print join("\n", @deblosses), "\n\n"; + } + + # We start by determining which files are in the first set of debs, the + # second set of debs or both. + my %files; + grep $files{$_}--, @D1; + grep $files{$_}++, @D2; + + my @old = sort grep $files{$_} < 0, keys %files; + my @new = sort grep $files{$_} > 0, keys %files; + my @same = sort grep $files{$_} == 0, keys %files; + + # We store any changed files in a hash of hashes %changes, where + # $changes{$from}{$to} is an array of files which have moved + # from package $from to package $to; $from or $to is '-' if + # the files have appeared or disappeared + + my %changes; + my @funny; # for storing changed files which appear in multiple debs + + foreach my $file (@old) { + my @firstdebs = split /:/, $files1{$file}; + foreach my $firstdeb (@firstdebs) { + push @{ $changes{$firstdeb}{'-'} }, $file; + } + } + + foreach my $file (@new) { + my @seconddebs = split /:/, $files2{$file}; + foreach my $seconddeb (@seconddebs) { + push @{ $changes{'-'}{$seconddeb} }, $file; + } + } + + foreach my $file (@same) { + # Are they identical? + next if $files1{$file} eq $files2{$file}; + + # Ah, they're not the same. If the file has moved from one deb + # to another, we'll put a note in that pair. But if the file + # was in more than one deb or ends up in more than one deb, we'll + # list it separately. + my @fdebs1 = split(/:/, $files1{$file}); + my @fdebs2 = split(/:/, $files2{$file}); + + if (@fdebs1 == 1 && @fdebs2 == 1) { + push @{ $changes{ $fdebs1[0] }{ $fdebs2[0] } }, $file; + } else { + # two packages to one or vice versa, or something like that + push @funny, [$file, \@fdebs1, \@fdebs2]; + } + } + + # This is not a very efficient way of doing things if there are + # lots of debs involved, but since that is highly unlikely, it + # shouldn't be much of an issue + my $changed = 0; + + for my $deb1 (sort(keys %debs1), '-') { + next unless exists $changes{$deb1}; + for my $deb2 ('-', sort keys %debs2) { + next unless exists $changes{$deb1}{$deb2}; + my $msg; + if (!$changed) { + print +"[The following lists of changes regard files as different if they have\ndifferent names, permissions or owners.]\n\n"; + } + if ($deb1 eq '-') { + $msg + = "New files in second set of .debs, found in package $deb2"; + } elsif ($deb2 eq '-') { + $msg + = "Files only in first set of .debs, found in package $deb1"; + } else { + $msg = "Files moved from package $deb1 to package $deb2"; + } + print $msg, "\n", '-' x length $msg, "\n"; + print join("\n", @{ $changes{$deb1}{$deb2} }), "\n\n"; + $changed = 1; + } + } + + if (@funny) { + my $msg + = "Files moved or copied from at least TWO packages or to at least TWO packages"; + print $msg, "\n", '-' x length $msg, "\n"; + for my $funny (@funny) { + print $$funny[0], "\n"; # filename and details + print "From package", (@{ $$funny[1] } > 1 ? "s" : ""), ": "; + print join(", ", @{ $$funny[1] }), "\n"; + print "To package", (@{ $$funny[2] } > 1 ? "s" : ""), ": "; + print join(", ", @{ $$funny[2] }), "\n"; + } + $changed = 1; + } + + if (!$quiet && !$changed) { + print + "File lists identical on package level (after any substitutions)\n"; + } + $exit_status = 1 if $changed; +} else { + my %files; + grep $files{$_}--, @D1; + grep $files{$_}++, @D2; + + my @losses = sort grep $files{$_} < 0, keys %files; + my @gains = sort grep $files{$_} > 0, keys %files; + + if (@losses == 0 && @gains == 0) { + print "File lists identical (after any substitutions)\n" + unless $quiet; + } else { + print +"[The following lists of changes regard files as different if they have\ndifferent names, permissions or owners.]\n\n"; + } + + if (@gains) { + my $msg; + if ($type eq 'debs') { + $msg = "Files in second set of .debs but not in first"; + } else { + $msg = sprintf "Files in second .%s but not in first", + $type eq 'deb' ? 'deb' : 'changes'; + } + print $msg, "\n", '-' x length $msg, "\n"; + print join("\n", @gains), "\n"; + $exit_status = 1; + } + + if (@losses) { + print "\n" if @gains; + my $msg; + if ($type eq 'debs') { + $msg = "Files in first set of .debs but not in second"; + } else { + $msg = sprintf "Files in first .%s but not in second", + $type eq 'deb' ? 'deb' : 'changes'; + } + print $msg, "\n", '-' x length $msg, "\n"; + print join("\n", @losses), "\n"; + $exit_status = 1; + } +} + +# We compare the control files (at least the dependency fields) +if (defined $singledeb[1] and defined $singledeb[2]) { + @CommonDebs = ($dummyname); + $DebPaths1{$dummyname} = $singledeb[1]; + $DebPaths2{$dummyname} = $singledeb[2]; +} + +exit $exit_status unless (@CommonDebs > 0) and $compare_control; + +unless (system("command -v wdiff >/dev/null 2>&1") == 0) { + warn "Can't compare control files; wdiff package not installed\n"; + exit $exit_status; +} + +for my $debname (@CommonDebs) { + no strict 'refs'; + mktmpdirs(); + + for my $i (1, 2) { + my $debpath = "${\"DebPaths$i\"}{$debname}"; + my $diri = ${"dir$i"}; + eval { + spawn( + exec => ['dpkg-deb', '-e', $debpath, $diri], + wait_child => 1 + ); + }; + if ($@) { + my $msg = "dpkg-deb -e ${\"DebPaths$i\"}{$debname} failed!"; + rmtree([$dir1, $dir2]); + fatal $msg; + } + } + + use strict 'refs'; + $exit_status = wdiff_control_files($dir1, $dir2, $debname, $controlfiles, + $exit_status); + + # Clean up + rmtree([$dir1, $dir2]); +} + +exit $exit_status; + +###### Subroutines + +# This routine takes the output of dpkg-deb -c and returns +# a processed listref +sub process_debc($$) { + my ($data, $number) = @_; + my (@filelist); + + # Format of dpkg-deb -c output: + # permissions owner/group size date time name ['->' link destination] + $data =~ s/^(\S+)\s+(\S+)\s+(\S+\s+){3}/$1 $2 /mg; + $data =~ s, \./, /,mg; + @filelist = grep !m| /$|, split /\n/, $data; # don't bother keeping '/' + + # Are we keeping directory names in our filelists? + if ($ignore_dirs) { + @filelist = grep !m|/$|, @filelist; + } + + # Do the "move" substitutions in the order received for the first debs + if ($number == 1 and @move) { + my @split_filelist + = map { m/^(\S+) (\S+) (.*)/ && [$1, $2, $3] } @filelist; + for my $move (@move) { + my $regex = $$move[0]; + my $from = $$move[1]; + my $to = $$move[2]; + map { + if ($regex) { eval "\$\$_[2] =~ s:$from:$to:g"; } + else { $$_[2] =~ s/\Q$from\E/$to/; } + } @split_filelist; + } + @filelist = map { "$$_[0] $$_[1] $$_[2]" } @split_filelist; + } + + return \@filelist; +} + +# This does the same for dpkg-deb -I +sub process_debI($) { + my ($data) = @_; + my (@filelist); + + # Format of dpkg-deb -c output: + # 2 (always?) header lines + # nnnn bytes, nnn lines [*] filename [interpreter] + # Package: ... + # rest of control file + + foreach (split /\n/, $data) { + last if /^Package:/; + next unless /^\s+\d+\s+bytes,\s+\d+\s+lines\s+(\*)?\s+([\-\w]+)/; + my $control = $2; + my $perms = ($1 ? "-rwxr-xr-x" : "-rw-r--r--"); + push @filelist, "$perms root/root DEBIAN/$control"; + } + + return \@filelist; +} + +sub wdiff_control_files($$$$$) { + my ($dir1, $dir2, $debname, $controlfiles, $origstatus) = @_; + return + unless defined $dir1 + and defined $dir2 + and defined $debname + and defined $controlfiles; + my @cf; + my $status = $origstatus; + if ($controlfiles eq 'ALL') { + # only need to list one directory as we are only comparing control + # files in both packages + @cf = grep { !/md5sums/ } map { basename($_); } glob("$dir1/*"); + } else { + @cf = split /,/, $controlfiles; + } + + foreach my $cf (@cf) { + next unless -f "$dir1/$cf" and -f "$dir2/$cf"; + if ($cf eq 'control' or $cf eq 'conffiles' or $cf eq 'shlibs') { + for my $file ("$dir1/$cf", "$dir2/$cf") { + my ($fd, @hdrs); + open $fd, '<', $file or fatal "Cannot read $file: $!"; + while (<$fd>) { + if (/^\s/ and @hdrs > 0) { + $hdrs[$#hdrs] .= $_; + } else { + push @hdrs, $_; + } + } + close $fd; + chmod 0644, $file; + open $fd, '>', $file or fatal "Cannot write $file: $!"; + print $fd sort @hdrs; + close $fd; + } + } + my $usepkgname = $debname eq $dummyname ? "" : " of package $debname"; + my @opts = ('-n'); + push @opts, $wdiff_opt if $wdiff_opt; + my ($wdiff, $wdiff_error) = ('', ''); + spawn( + exec => ['wdiff', @opts, "$dir1/$cf", "$dir2/$cf"], + to_string => \$wdiff, + error_to_string => \$wdiff_error, + wait_child => 1, + nocheck => 1 + ); + if ($? && ($? >> 8) != 1) { + print "$wdiff_error\n"; + warn "wdiff failed\n"; + } else { + if (!$?) { + if (!$quiet) { + print +"\nNo differences were encountered between the $cf files$usepkgname\n"; + } + } elsif ($wdiff_opt) { + # Don't try messing with control codes + my $msg = ucfirst($cf) . " files$usepkgname: wdiff output"; + print "\n", $msg, "\n", '-' x length $msg, "\n"; + print $wdiff; + $status = 1; + } else { + my @output; + @output = split /\n/, $wdiff; + @output = grep /(\[-|\{\+)/, @output; + my $msg = ucfirst($cf) + . " files$usepkgname: lines which differ (wdiff format)"; + print "\n", $msg, "\n", '-' x length $msg, "\n"; + print join("\n", @output), "\n"; + $status = 1; + } + } + } + + return $status; +} + +sub mktmpdirs () { + no strict 'refs'; + + for my $i (1, 2) { + ${"dir$i"} = tempdir(CLEANUP => 1); + fatal "Couldn't create temp directory" + if not defined ${"dir$i"}; + } +} + +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/; + die $msg; +} diff --git a/scripts/debi.1 b/scripts/debi.1 new file mode 100644 index 0000000..aa263a8 --- /dev/null +++ b/scripts/debi.1 @@ -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>. diff --git a/scripts/debi.bash_completion b/scripts/debi.bash_completion new file mode 100644 index 0000000..4fa10df --- /dev/null +++ b/scripts/debi.bash_completion @@ -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 : diff --git a/scripts/debi.pl b/scripts/debi.pl new file mode 100755 index 0000000..7e10f53 --- /dev/null +++ b/scripts/debi.pl @@ -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; diff --git a/scripts/debootsnap b/scripts/debootsnap new file mode 100755 index 0000000..81297f5 --- /dev/null +++ b/scripts/debootsnap @@ -0,0 +1,694 @@ +#!/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 +# 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: Adress invalid names +# pylint: disable=invalid-name + +import argparse +import dataclasses +import http.server +import os +import pathlib +import re +import shutil +import socketserver +import subprocess +import sys +import tempfile +import threading +import time +from collections import defaultdict +from contextlib import contextmanager +from functools import partial +from http import HTTPStatus +from operator import itemgetter + +import pycurl +import requests +from debian.deb822 import BuildInfo + + +class MyHTTPException(Exception): + pass + + +class MyHTTP404Exception(Exception): + pass + + +class MyHTTPTimeoutException(Exception): + pass + + +class RetryCountExceeded(Exception): + pass + + +# pylint: disable=c-extension-no-member +class Proxy(http.server.SimpleHTTPRequestHandler): + last_request = None + maxretries = 10 + + def do_GET(self): # pylint: disable=too-many-branches,too-many-statements + # check validity and extract the timestamp + url = "http://snapshot.debian.org/" + self.path + start = None + state = "" + written = 0 + for retrynum in range(self.maxretries): + try: + c = pycurl.Curl() + c.setopt(c.URL, url) + # even 100 kB/s is too much sometimes + c.setopt(c.MAX_RECV_SPEED_LARGE, 1000 * 1024) # bytes per second + c.setopt(c.CONNECTTIMEOUT, 30) # the default is 300 + # sometimes, curl stalls forever and even ctrl+c doesn't work + start = time.time() + + def progress(*_): + # a download must not last more than 10 minutes + # with 100 kB/s this means files cannot be larger than 62MB + if time.time() - start > 10 * 60: + print("transfer took too long") + # the code will not see this exception but instead get a + # pycurl.error + raise MyHTTPTimeoutException(url) + + c.setopt(pycurl.NOPROGRESS, 0) + c.setopt(pycurl.XFERINFOFUNCTION, progress) + # $ host snapshot.debian.org + # snapshot.debian.org has address 185.17.185.185 + # snapshot.debian.org has address 193.62.202.27 + # c.setopt(c.RESOLVE, ["snapshot.debian.org:80:185.17.185.185"]) + if written > 0: + c.setopt(pycurl.RESUME_FROM, written) + + def writer_cb(data): + assert state == "headers sent", state + nonlocal written + written += len(data) + return self.wfile.write(data) + + c.setopt(c.WRITEFUNCTION, writer_cb) + + # using a header callback allows us to send headers of our own + # with the correct content-length value out without having to + # wait for perform() to finish + def header_cb(line): + nonlocal state + # if this is a retry, then the headers have already been + # sent and there is nothing to do + if state == "headers sent": + return + # HTTP standard specifies that headers are encoded in iso-8859-1 + line = line.decode("iso-8859-1").rstrip() + # the first try must be a http 200 + if line == "HTTP/1.1 200 OK": + assert state == "" + self.send_response(HTTPStatus.OK) + state = "http200 sent" + return + # the header is done + if line == "": + assert state == "length sent" + self.end_headers() + state = "headers sent" + return + field, value = line.split(":", 1) + field = field.strip().lower() + value = value.strip() + # we are only interested in content-length + if field != "content-length": + return + assert state == "http200 sent" + self.send_header("Content-Length", value) + state = "length sent" + + c.setopt(c.HEADERFUNCTION, header_cb) + c.perform() + if c.getinfo(c.RESPONSE_CODE) == 404: + raise MyHTTP404Exception(f"got HTTP 404 for {url}") + if c.getinfo(c.RESPONSE_CODE) not in [200, 206]: + raise MyHTTPException( + f"got HTTP {c.getinfo(c.RESPONSE_CODE)} for {url}" + ) + c.close() + # if the requests finished too quickly, sleep the remaining time + # s/r r/h + # 3 1020 + # 2.5 1384 + # 2.4 1408 + # 2 1466 + # 1.5 2267 + seconds_per_request = 1.5 + if self.last_request is not None: + sleep_time = seconds_per_request - (time.time() - self.last_request) + if sleep_time > 0: + time.sleep(sleep_time) + self.last_request = time.time() + break + except pycurl.error as e: + code, _ = e.args + if code in [ + pycurl.E_PARTIAL_FILE, + pycurl.E_COULDNT_CONNECT, + pycurl.E_ABORTED_BY_CALLBACK, + ]: + if retrynum == self.maxretries - 1: + break + if code == pycurl.E_ABORTED_BY_CALLBACK: + # callback was aborted due to timeout + pass + sleep_time = 4 ** (retrynum + 1) + print(f"retrying after {sleep_time} s...") + time.sleep(sleep_time) + continue + raise + except MyHTTPException as e: + print("got HTTP error:", repr(e)) + if retrynum == self.maxretries - 1: + break + sleep_time = 4 ** (retrynum + 1) + print(f"retrying after {sleep_time} s...") + time.sleep(sleep_time) + # restart from the beginning or otherwise, the result might + # include a varnish cache error message + else: + raise RetryCountExceeded("failed too often...") + + +@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 parse_args(args: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description="""\ + +Combines debootstrap and snapshot.debian.org to create a chroot 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 + +""", + ) + 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", + ) + 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", + ) + parser.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", + ) + parser.add_argument( + "output", nargs="?", default="-", help="path to output chroot tarball" + ) + return parser.parse_args(args) + + +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_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" + ) + + 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", + '--aptopt=Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"', + '--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 + + +@contextmanager +def proxy_snapshot(tmpdirname): + httpd = socketserver.TCPServer( + # the default address family for socketserver is AF_INET so we + # explicitly bind to ipv4 localhost + ("localhost", 0), + partial(Proxy, 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 download_packages( + tmpdirname, sources: list[Source], pkgs, nativearch, foreignarches +): + 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") + + with proxy_snapshot(tmpdirname) as port: + with open(tmpdirname + "/etc/apt/sources.list", "w", encoding="utf8") as f: + for source in sources: + f.write(source.deb_line(f"localhost:{port}")) + subprocess.check_call( + ["apt-get", "update", "--error-on=any"], + env={"APT_CONFIG": tmpdirname + "/apt.conf"}, + ) + for i, nav in enumerate(pkgs): + print(f"{i + 1} of {len(pkgs)}") + with tempfile.TemporaryDirectory() as tmpdir2: + subprocess.check_call( + ["apt-get", "download", "--yes", f"{nav[0]}:{nav[1]}={nav[2]}"], + cwd=tmpdir2, + env={"APT_CONFIG": tmpdirname + "/apt.conf"}, + ) + 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], tmpdirname + "/cache") + + +def main(arguments: list[str]) -> None: + args = parse_args(arguments) + if args.packages: + pkgs = [v for sublist in args.packages for v in sublist] + if args.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 = args.architecture + 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: + download_packages(tmpdirname, sources, pkgs, nativearch, foreignarches) + + create_repo(tmpdirname, pkgs) + + newpkgs = run_mmdebstrap( + tmpdirname, sources, nativearch, foreignarches, args.output + ) + + # make sure that the installed packages match the requested package + # list + assert set(newpkgs) == set(pkgs) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/scripts/debootsnap.py b/scripts/debootsnap.py new file mode 120000 index 0000000..1117123 --- /dev/null +++ b/scripts/debootsnap.py @@ -0,0 +1 @@ +debootsnap
\ No newline at end of file diff --git a/scripts/debrebuild.pl b/scripts/debrebuild.pl new file mode 100755 index 0000000..561db86 --- /dev/null +++ b/scripts/debrebuild.pl @@ -0,0 +1,715 @@ +#!/usr/bin/perl +# +# Copyright © 2014-2020 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::Index; +use Dpkg::Deps; +use Dpkg::Source::Package; +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 List::Util qw(any none); + +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?"; + } + } + +} + +my $respect_build_path = 1; +my $use_tor = 0; +my $outdir = './'; +my $builder = 'none'; + +my %OPTIONS = ( + 'help|h' => sub { usage(0); }, + 'use-tor-proxy!' => \$use_tor, + 'respect-build-path!' => \$respect_build_path, + 'buildresult=s' => \$outdir, + 'builder=s' => \$builder, +); + +sub usage { + my ($exit_code) = @_; + $exit_code //= 0; + print <<EOF; +Usage: $progname [options] <buildinfo> + $progname <--help|-h> + +Given a buildinfo file from a Debian package, generate instructions for +attempting to reproduce the binary packages built from the associated source +and build information. + +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. + --buildresults 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 --buildresults=./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); + +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 contained a GPG signature; it has NOT been validated (debrebuild does not support this)!\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}; +{ + # 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/; + } +} + +my $srcpkgbinver + = $cdata->{Version}; # this version will include the binmu suffix + +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 "refusing to overwrite the input buildinfo file\n"; + } +} + +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 $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 @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}; + } + 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 $tarballpath = ''; +my $sourceslist = ''; +if (any { $_ eq $builder } ('none', 'dpkg')) { + open my $fh, '-|', 'debootsnap', "--buildinfo=$buildinfo", + '--sources-list-only' // die "cannot exec debootsnap"; + $sourceslist = do { local $/; <$fh> }; + close $fh; +} elsif (any { $_ eq $builder } ('mmdebstrap', 'sbuild', 'sbuild+unshare')) { + (undef, $tarballpath) + = tempfile('debrebuild.tar.XXXXXXXXXXXX', OPEN => 0, TMPDIR => 1); + 0 == system 'debootsnap', "--buildinfo=$buildinfo", $tarballpath + or die "debootsnap 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', + $srcpkg->get_basename(1) . '.dsc', $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") { + + my @cmd = ('env', "--chdir=$outdir", @environment, '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-autopkgtest"; + push @cmd, "--no-apt-upgrade"; + push @cmd, "--no-apt-distupgrade"; + # disable the explainer + push @cmd, "--bd-uninstallable-explainer="; + + if ($custom_build_path) { + push @cmd, "--build-path=$custom_build_path"; + } + push @cmd, "${srcpkgname}_$srcpkgver"; + print((join " ", @cmd) . "\n"); + 0 == system @cmd or die "sbuild failed\n"; +} 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', + "--setup-hook=tar --exclude=\"./dev/*\" -C \"\$1\" -xf " + . (String::ShellQuote::shell_quote $tarballpath), + '--setup-hook=rm "$1"/etc/apt/sources.list', +"--customize-hook=debsnap --force --destdir \"\$1\" $srcpkgname $srcpkgver", + '--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"); + } +} diff --git a/scripts/debrelease.1 b/scripts/debrelease.1 new file mode 100644 index 0000000..48d0f4a --- /dev/null +++ b/scripts/debrelease.1 @@ -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>. diff --git a/scripts/debrelease.sh b/scripts/debrelease.sh new file mode 100755 index 0000000..b468de0 --- /dev/null +++ b/scripts/debrelease.sh @@ -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 diff --git a/scripts/debrepro.pod b/scripts/debrepro.pod new file mode 100644 index 0000000..6cc7b2d --- /dev/null +++ b/scripts/debrepro.pod @@ -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>. diff --git a/scripts/debrepro.sh b/scripts/debrepro.sh new file mode 100755 index 0000000..b8b309b --- /dev/null +++ b/scripts/debrepro.sh @@ -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 diff --git a/scripts/debrsign.1 b/scripts/debrsign.1 new file mode 100644 index 0000000..b4eacae --- /dev/null +++ b/scripts/debrsign.1 @@ -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. diff --git a/scripts/debrsign.sh b/scripts/debrsign.sh new file mode 100755 index 0000000..128f88b --- /dev/null +++ b/scripts/debrsign.sh @@ -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 diff --git a/scripts/debsign.1 b/scripts/debsign.1 new file mode 100644 index 0000000..900a61c --- /dev/null +++ b/scripts/debsign.1 @@ -0,0 +1,146 @@ +.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 gpg2 (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. diff --git a/scripts/debsign.bash_completion b/scripts/debsign.bash_completion new file mode 100644 index 0000000..a7b72fb --- /dev/null +++ b/scripts/debsign.bash_completion @@ -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 GnuPG 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 : diff --git a/scripts/debsign.sh b/scripts/debsign.sh new file mode 100755 index 0000000..15b0dfc --- /dev/null +++ b/scripts/debsign.sh @@ -0,0 +1,870 @@ +#!/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" + + gpgversion=$($signcommand --version | head -n 1 | cut -d' ' -f3) + gpgmajorversion=$(echo $gpgversion | cut -d. -f1) + gpgminorversion=$(echo $gpgversion | cut -d. -f2) + + if [ $gpgmajorversion -gt 1 -o $gpgminorversion -ge 4 ] + then + $signcommand --no-auto-check-trustdb \ + --local-user "$signas" --clearsign \ + --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 + } + else + $signcommand --local-user "$signas" --clearsign \ + --no-show-policy-url \ + --armor --textmode --output "$ASCII_SIGNED_FILE" \ + "$UNSIGNED_FILE" || \ + { SAVESTAT=$? + echo "$PROGNAME: $signcommand error occurred! Aborting...." >&2 + stty $savestty 2>/dev/null || true + exit $SAVESTAT + } + fi + 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 + elif command -v gpg2 > /dev/null; then + signcommand=gpg2 + 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 diff --git a/scripts/debsnap.1 b/scripts/debsnap.1 new file mode 100644 index 0000000..7958e9e --- /dev/null +++ b/scripts/debsnap.1 @@ -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` + diff --git a/scripts/debsnap.pl b/scripts/debsnap.pl new file mode 100755 index 0000000..479e80c --- /dev/null +++ b/scripts/debsnap.pl @@ -0,0 +1,423 @@ +#!/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(); + + # 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 ($warnings) { + exit 2; +} +exit 0; diff --git a/scripts/debuild.1 b/scripts/debuild.1 new file mode 100644 index 0000000..2232c73 --- /dev/null +++ b/scripts/debuild.1 @@ -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>. diff --git a/scripts/debuild.bash_completion b/scripts/debuild.bash_completion new file mode 100644 index 0000000..22787bc --- /dev/null +++ b/scripts/debuild.bash_completion @@ -0,0 +1,103 @@ +# /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 : diff --git a/scripts/debuild.pl b/scripts/debuild.pl new file mode 100755 index 0000000..fa6f94c --- /dev/null +++ b/scripts/debuild.pl @@ -0,0 +1,1229 @@ +#!/usr/bin/perl + +# Perl version of Christoph Lameter's build program, renamed debuild. +# Written by Julian Gilbey, December 1998. + +# 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/>. + +# We will do simple option processing. The calling syntax of this +# program is: +# +# debuild [<debuild options>] -- binary|binary-arch|binary-indep|clean ... +# or +# debuild [<debuild options>] [<dpkg-buildpackage options>] +# [--lintian-opts <lintian options>] +# +# In the first case, debuild will simply run debian/rules with the +# given parameter. Available options are listed in usage() below. +# +# In the second case, the behaviour is to run dpkg-buildpackage and +# then to run lintian on the resulting .changes file. +# Lintian options may be specified after --lintian-opts; all following +# options will be passed only to lintian. +# +# As this may be running setuid, we make sure to clean out the +# environment before we perform the build, subject to any -e etc. +# options. Also wise for building the packages, anyway. +# We don't put /usr/local/bin in the PATH as Debian +# programs will presumably be built without the use of any locally +# installed programs. This could be changed, but in which case, +# please add /usr/local/bin at the END so that you don't get any +# unexpected behaviour. + +# We will try to preserve the locale variables, but if it turns out that +# this harms the package building process, we will clean them out too. +# Please file a bug report if this is the case! + +use strict; +use warnings; +use 5.008; +use File::Basename; +use filetest 'access'; +use Cwd; +use Dpkg::Changelog::Parse qw(changelog_parse); +use Dpkg::IPC; +use IO::Handle; # for flushing +use vars qw(*BUILD *OLDOUT *OLDERR); # prevent a warning + +my $progname = basename($0); +my $modified_conf_msg; +my @warnings; + +# Predeclare functions +sub setDebuildHook; +sub setDpkgHook; +sub system_withecho(@); +sub run_hook ($$); +sub fatal($); + +sub usage { + print <<"EOF"; +First usage method: + $progname [<debuild options>] -- binary|binary-arch|binary-indep|clean ... + to run debian/rules with given parameter(s). Options here are + --no-conf, --noconf Don\'t read devscripts config files; + must be the first option given + --rootcmd=<gain-root-command>, -r<gain-root-command> + Command used to become root if $progname + not setuid root and the package needs (fake)root + (dpkg-buildpackage uses fakeroot by default if + not provided) + + --preserve-envvar=<envvar>, -e<envvar> + Preserve environment variable <envvar> + + --preserve-env Preserve all environment vars (except PATH) + + --set-envvar=<envvar>=<value>, -e<envvar>=<value> + Set environment variable <envvar> to <value> + + --prepend-path=<value> Prepend <value> to the sanitised PATH + + -d Skip checking of build dependencies + -D Force checking of build dependencies (default) + + --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, -h display this message + + --version show version and copyright information + +Second usage method: + $progname [<debuild options>] [<dpkg-buildpackage options>] + [--lintian-opts <lintian options>] + to run dpkg-buildpackage and then run lintian on the resulting + .changes file. + + Additional debuild options available in this case are: + + --lintian Run lintian (default) + --no-lintian Do not run lintian + --[no-]tgz-check Do [not] check for an .orig.tar.gz before running + dpkg-buildpackage if we have a Debian revision + (Default: check) + --username Run debrsign instead of debsign, using the + supplied credentials + + --dpkg-buildpackage-hook=HOOK + --clean-hook=HOOK + --dpkg-source-hook=HOOK + --build-hook=HOOK + --binary-hook=HOOK + --dpkg-genchanges-hook=HOOK + --final-clean-hook=HOOK + --lintian-hook=HOOK + --signing-hook=HOOK + --post-dpkg-buildpackage-hook=HOOK + These hooks run at the various stages of the + dpkg-buildpackage run. For details, see the + debuild manpage. They default to nothing, and + can be reset to nothing with --foo-hook='' + --clear-hooks Clear all hooks + + For available dpkg-buildpackage and lintian options, see their + respective manpages. + +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### +This code is copyright 1999-2003 by Julian Gilbey <jdg\@debian.org>, +all rights reserved. +Based on a shell-script program 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. +EOF +} + +# Start by reading configuration files and then command line +# The next stuff is somewhat boilerplate and somewhat not. +# It's complicated by the fact that the config files are in shell syntax, +# and we don't want to have to write a general shell parser in Perl. +# So we'll get the shell to do the work. Yuck. +# We allow DEBUILD_PRESERVE_ENVVARS="VAR1,VAR2,VAR3" +# and DEBUILD_SET_ENVVAR_VAR1=VAL1, DEBUILD_SET_ENVVAR_VAR2=VAR2. + +# Set default values before we start +my $preserve_env = 0; +my %save_vars; +my $root_command = ''; +my $run_lintian = 1; +my $lintian_exists = 0; +my @dpkg_extra_opts = (); +my @lintian_extra_opts = (); +my @lintian_opts = (); +my $checkbuilddep; +my $check_dirname_level = 1; +my $check_dirname_regex = 'PACKAGE(-.+)?'; +my $logging = 0; +my $tgz_check = 1; +my $prepend_path = ''; +my $username = ''; +my @hooks = ( + qw(dpkg-buildpackage clean dpkg-source build binary dpkg-genchanges + final-clean lintian signing post-dpkg-buildpackage) +); +my %hook; +@hook{@hooks} = ('') x @hooks; +# dpkg-buildpackage runs all hooks in the source tree, while debuild runs some +# in the parent directory. Use %externalHook to check which run out of tree +my %externalHook; +@externalHook{@hooks} = (0) x @hooks; +$externalHook{lintian} = 1; +$externalHook{signing} = 1; +$externalHook{'post-dpkg-buildpackage'} = 1; +# Track which hooks are run by dpkg-buildpackage vs. debuild +my %dpkgHook; +@dpkgHook{@hooks} = (1) x @hooks; +$dpkgHook{lintian} = 0; +$dpkgHook{signing} = 0; +$dpkgHook{'post-dpkg-buildpackage'} = 0; + +# First handle private options from cvs-debuild +my ($cvsdeb_file, $cvslin_file); +if (@ARGV and $ARGV[0] eq '--cvs-debuild') { + shift; + $check_dirname_level = 0; # no need to check dirnames if we're being + # called from cvs-debuild + if (@ARGV and $ARGV[0] eq '--cvs-debuild-deb') { + shift; + $cvsdeb_file = shift; + unless ($cvsdeb_file =~ m%^/dev/fd/\d+$%) { + fatal + "--cvs-debuild-deb is an internal option and should not be used"; + } + } + if (@ARGV and $ARGV[0] eq '--cvs-debuild-lin') { + shift; + $cvslin_file = shift; + unless ($cvslin_file =~ m%^/dev/fd/\d+$%) { + fatal + "--cvs-debuild-lin is an internal option and should not be used"; + } + } + if (defined $cvsdeb_file) { + local $/; + open DEBOPTS, $cvsdeb_file + or fatal "can't open cvs-debuild debuild options file: $!"; + my $opts = <DEBOPTS>; + close DEBOPTS; + + unshift @ARGV, split(/\0/, $opts, -1); + } + if (defined $cvslin_file) { + local $/; + open LINOPTS, $cvslin_file + or fatal "can't open cvs-debuild lin* options file: $!"; + my $opts = <LINOPTS>; + close LINOPTS; + + push @ARGV, split(/\0/, $opts, -1); + } +} + +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 = ( + 'DEBUILD_PRESERVE_ENV' => 'no', + 'DEBUILD_PRESERVE_ENVVARS' => '', + 'DEBUILD_LINTIAN' => 'yes', + 'DEBUILD_ROOTCMD' => $root_command, + 'DEBUILD_TGZ_CHECK' => 'yes', + 'DEBUILD_DPKG_BUILDPACKAGE_HOOK' => '', + 'DEBUILD_CLEAN_HOOK' => '', + 'DEBUILD_DPKG_SOURCE_HOOK' => '', + 'DEBUILD_BUILD_HOOK' => '', + 'DEBUILD_BINARY_HOOK' => '', + 'DEBUILD_DPKG_GENCHANGES_HOOK' => '', + 'DEBUILD_FINAL_CLEAN_HOOK' => '', + 'DEBUILD_LINTIAN_HOOK' => '', + 'DEBUILD_SIGNING_HOOK' => '', + 'DEBUILD_PREPEND_PATH' => '', + 'DEBUILD_POST_DPKG_BUILDPACKAGE_HOOK' => '', + 'DEBUILD_SIGNING_USERNAME' => '', + 'DEVSCRIPTS_CHECK_DIRNAME_LEVEL' => 1, + 'DEVSCRIPTS_CHECK_DIRNAME_REGEX' => 'PACKAGE(-.+)?', + ); + my %config_default = %config_vars; + my $dpkg_opts_var = 'DEBUILD_DPKG_BUILDPACKAGE_OPTS'; + my $lintian_opts_var = 'DEBUILD_LINTIAN_OPTS'; + + my $shell_cmd; + # Set defaults + $shell_cmd .= qq[unset `set | grep "^DEBUILD_" | cut -d= -f1`;\n]; + foreach my $var (keys %config_vars) { + $shell_cmd .= qq[$var="$config_vars{$var}";\n]; + } + foreach my $var ($dpkg_opts_var, $lintian_opts_var) { + $shell_cmd .= "$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" } + foreach my $var ($dpkg_opts_var, $lintian_opts_var) { + $shell_cmd .= "eval set -- \$$var;\n"; + $shell_cmd .= "echo \">>> $var BEGIN <<<\";\n"; + $shell_cmd + .= 'while [ $# -gt 0 ]; do printf "%s\n" "$1"; shift; done;' . "\n"; + $shell_cmd .= "echo \">>> $var END <<<\";\n"; + } + # Not totally efficient, but never mind + $shell_cmd + .= 'for var in `set | grep "^DEBUILD_SET_ENVVAR_" | cut -d= -f1`; do '; + $shell_cmd .= 'eval echo $var=\$$var; done;' . "\n"; + # print STDERR "Running shell command:\n$shell_cmd"; + my $shell_out = `/bin/bash -c '$shell_cmd'`; + # print STDERR "Shell output:\n${shell_out}End shell output\n"; + my @othervars; + (@config_vars{ keys %config_vars }, @othervars) = split /\n/, $shell_out, + -1; + + # Check validity + $config_vars{'DEBUILD_PRESERVE_ENV'} =~ /^(yes|no)$/ + or $config_vars{'DEBUILD_PRESERVE_ENV'} = 'no'; + $config_vars{'DEBUILD_LINTIAN'} =~ /^(yes|no)$/ + or $config_vars{'DEBUILD_LINTIAN'} = 'yes'; + $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'} =~ /^[012]$/ + or $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'} = 1; + $config_vars{'DEBUILD_TGZ_CHECK'} =~ /^(yes|no)$/ + or $config_vars{'DEBUILD_TGZ_CHECK'} = 'yes'; + + foreach my $var (sort keys %config_vars) { + if ($config_vars{$var} ne $config_default{$var}) { + $modified_conf_msg .= " $var=$config_vars{$var}\n"; + } + } + + # What did we find? + $preserve_env = $config_vars{'DEBUILD_PRESERVE_ENV'} eq 'yes' ? 1 : 0; + if ($config_vars{'DEBUILD_PRESERVE_ENVVARS'} ne '') { + my @preserve_vars = split /\s*,\s*/, + $config_vars{'DEBUILD_PRESERVE_ENVVARS'}; + foreach my $index (0 .. $#preserve_vars) { + my $var = $preserve_vars[$index]; + if ($var =~ /\*$/) { + $var =~ s/([^.])\*$/$1.\*/; + my @vars = grep /^$var$/, keys %ENV; + push @preserve_vars, @vars; + delete $preserve_vars[$index]; + } + } + @preserve_vars = map { $_ if defined $_ } @preserve_vars; + @save_vars{@preserve_vars} = (1) x scalar @preserve_vars; + } + $run_lintian = $config_vars{'DEBUILD_LINTIAN'} eq 'no' ? 0 : 1; + $root_command = $config_vars{'DEBUILD_ROOTCMD'}; + $tgz_check = $config_vars{'DEBUILD_TGZ_CHECK'} eq 'yes' ? 1 : 0; + $prepend_path = $config_vars{'DEBUILD_PREPEND_PATH'}; + $username = $config_vars{'DEBUILD_SIGNING_USERNAME'}; + $check_dirname_level = $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_LEVEL'}; + $check_dirname_regex = $config_vars{'DEVSCRIPTS_CHECK_DIRNAME_REGEX'}; + + for my $hookname (@hooks) { + my $config_name = uc "debuild_${hookname}_hook"; + $config_name =~ tr/-/_/; + setDebuildHook($hookname, $config_vars{$config_name}); + } + + # Now parse the opts lists + if (shift @othervars ne ">>> $dpkg_opts_var BEGIN <<<") { + fatal "internal error: dpkg opts list missing proper header"; + } + while (($_ = shift @othervars) ne ">>> $dpkg_opts_var END <<<" + and @othervars) { + push @dpkg_extra_opts, $_; + } + if (!@othervars) { + fatal "internal error: dpkg opts list missing proper trailer"; + } + if (@dpkg_extra_opts) { + $modified_conf_msg + .= " $dpkg_opts_var='" . join(" ", @dpkg_extra_opts) . "'\n"; + } + + if (shift @othervars ne ">>> $lintian_opts_var BEGIN <<<") { + fatal "internal error: lintian opts list missing proper header"; + } + while (($_ = shift @othervars) ne ">>> $lintian_opts_var END <<<" + and @othervars) { + push @lintian_extra_opts, $_; + } + if (!@othervars) { + fatal "internal error: lintian opts list missing proper trailer"; + } + if (@lintian_extra_opts) { + $modified_conf_msg + .= " $lintian_opts_var='" . join(" ", @lintian_extra_opts) . "'\n"; + } + + # And what is left should be any ENV settings + foreach my $confvar (@othervars) { + $confvar =~ /^DEBUILD_SET_ENVVAR_([^=]*)=(.*)$/ or next; + $ENV{$1} = $2; + $save_vars{$1} = 1; + $modified_conf_msg .= " $1='$2'\n"; + } + + $modified_conf_msg ||= " (none)\n"; + chomp $modified_conf_msg; +} + +# We first check @dpkg_extra_opts for options which may affect us; +# these were set in a configuration file, so they have lower +# precedence than command line settings. The options we care about +# at this stage are: -r and those which affect the checkbuilddep setting + +foreach (@dpkg_extra_opts) { + /^-r(.*)$/ and $root_command = $1, next; + $_ eq '-d' and $checkbuilddep = 0, next; + $_ eq '-D' and $checkbuilddep = 1, next; +} + +# Check @ARGV for debuild options. +my @preserve_vars = qw(TERM HOME LOGNAME PGPPATH GNUPGHOME GPG_AGENT_INFO + DBUS_SESSION_BUS_ADDRESS GPG_TTY FAKEROOTKEY LANG DEBEMAIL); +@save_vars{@preserve_vars} = (1) x scalar @preserve_vars; +{ + no locale; + while (my $arg = shift) { + my $savearg = $arg; + my $opt = ''; + + $arg =~ /^(-h|--help)$/ and usage(), exit 0; + $arg eq '--version' and version(), exit 0; + + # Let's do the messy case first + if ($arg eq '--preserve-envvar') { + unless (defined($opt = shift)) { + fatal +"--preserve-envvar requires an argument,\nrun $progname --help for usage information"; + } + $savearg .= " $opt"; + } elsif ($arg =~ /^--preserve-envvar=(.*)/) { + $arg = '--preserve-envvar'; + $opt = $1; + } elsif ($arg eq '--set-envvar') { + unless (defined($opt = shift)) { + fatal +"--set-envvar requires an argument,\nrun $progname --help for usage information"; + } + $savearg .= " $opt"; + } elsif ($arg =~ /^--set-envvar=(.*)/) { + $arg = '--set-envvar'; + $opt = $1; + } + # dpkg-buildpackage now has a -e option, so we have to be + # careful not to confuse the two; their option will always have + # the form -e<maintainer email> or similar + elsif ($arg eq '-e') { + unless (defined($opt = shift)) { + fatal +"-e requires an argument,\nrun $progname --help for usage information"; + } + $savearg .= " $opt"; + if ($opt =~ /^\w+\*?$/) { $arg = '--preserve-envvar'; } + else { $arg = '--set-envvar'; } + } elsif ($arg =~ /^-e(\w+\*?)$/) { + $arg = '--preserve-envvar'; + $opt = $1; + } elsif ($arg =~ /^-e(\w+=.*)$/) { + $arg = '--set-envvar'; + $opt = $1; + } elsif ($arg =~ /^-e/) { + # seems like a dpkg-buildpackage option, so stop parsing + unshift @ARGV, $arg; + last; + } + + if ($arg eq '--preserve-envvar') { + if ($opt =~ /^\w+$/) { + $save_vars{$opt} = 1; + } elsif ($opt =~ /^\w+\*$/) { + $opt =~ s/([^.])\*$/$1.\*/; + my @vars = grep /^$opt$/, keys %ENV; + @save_vars{@vars} = (1) x scalar @vars; + } else { + push @warnings, + "Ignoring unrecognised/malformed option: $savearg"; + } + next; + } + if ($arg eq '--set-envvar') { + if ($opt =~ /^(\w+)=(.*)$/) { + $ENV{$1} = $2; + $save_vars{$1} = 1; + } else { + push @warnings, + "Ignoring unrecognised/malformed option: $savearg"; + } + next; + } + + $arg eq '--preserve-env' and $preserve_env = 1, next; + if ($arg eq '-E') { + push @warnings, +"-E is deprecated in debuild, as dpkg-buildpackage now uses it.\nPlease use --preserve-env instead in future.\n"; + $preserve_env = 1; + next; + } + $arg eq '--no-lintian' and $run_lintian = 0, next; + $arg eq '--lintian' and $run_lintian = 1, next; + if ($arg eq '--rootcmd') { + unless (defined($root_command = shift)) { + fatal +"--rootcmd requires an argument,\nrun $progname --help for usage information"; + } + next; + } + $arg =~ /^--rootcmd=(.*)/ and $root_command = $1, next; + if ($arg eq '-r') { + unless (defined($opt = shift)) { + fatal +"-r requires an argument,\nrun $progname --help for usage information"; + } + $root_command = $opt; + next; + } + $arg eq '--tgz-check' and $tgz_check = 1, next; + $arg =~ /^--no-?tgz-check$/ and $tgz_check = 0, next; + $arg =~ /^-r(.*)/ and $root_command = $1, next; + if ($arg =~ /^--check-dirname-level=(.*)$/) { + $arg = '--check-dirname-level'; + unshift @ARGV, $1; + } # fall through and let the next one handle it ;-) + if ($arg eq '--check-dirname-level') { + unless (defined($opt = shift)) { + fatal +"--check-dirname-level requires an argument,\nrun $progname --help for usage information"; + } + if ($opt =~ /^[012]$/) { $check_dirname_level = $opt; } + else { + fatal +"unrecognised --check-dirname-level value (allowed are 0,1,2)"; + } + next; + } + if ($arg eq '--check-dirname-regex') { + unless (defined($opt = shift)) { + fatal +"--check-dirname-regex requires an argument,\nrun $progname --help for usage information"; + } + $check_dirname_regex = $opt; + next; + } + if ($arg =~ /^--check-dirname-regex=(.*)$/) { + $check_dirname_regex = $1; + next; + } + + if ($arg eq '--prepend-path') { + unless (defined($opt = shift)) { + fatal +"--prepend-path requires an argument,\nrun $progname --help for usage information"; + } + $prepend_path = $opt; + next; + } + if ($arg =~ /^--prepend-path=(.*)$/) { + $prepend_path = $1; + next; + } + + if ($arg eq '--username') { + unless (defined($opt = shift)) { + fatal +"--username requires an argument,\nrun $progname --help for usage information"; + } + $username = $opt; + next; + } + if ($arg =~ /^--username=(.*)$/) { + $username = $1; + next; + } + + if ($arg =~ /^--no-?conf$/) { + fatal "$arg is only acceptable as the first command-line option!"; + } + $arg eq '-d' and $checkbuilddep = 0, next; + $arg eq '-D' and $checkbuilddep = 1, next; + + # hooks... + if ($arg =~ /^--(.*)-hook$/) { + my $argkey = $1; + unless (exists $hook{$argkey}) { + fatal +"unknown hook $arg,\nrun $progname --help for usage information"; + } + unless (defined($opt = shift)) { + fatal +"$arg requires an argument,\nrun $progname --help for usage information"; + } + + setDebuildHook($argkey, $opt); + next; + } + + if ($arg =~ /^--(.*?)-hook=(.*)/) { + my $argkey = $1; + my $opt = $2; + + unless (exists $hook{$argkey}) { + fatal +"unknown hook option $arg,\nrun $progname --help for usage information"; + } + + setDebuildHook($argkey, $opt); + next; + } + + if ($arg =~ /^--hook-(check|sign|done)=(.*)$/) { + my $name = $1; + my $opt = $2; + unless (defined($opt)) { + fatal +"$arg requires an argmuent,\nrun $progname --help for usage information"; + } + if ($name eq 'check') { + setDpkgHook('lintian', $opt); + } elsif ($name eq 'sign') { + setDpkgHook('signing', $opt); + } else { + setDpkgHook('post-dpkg-buildpackage', $opt); + } + next; + } + + if ($arg eq '--clear-hooks') { $hook{@hooks} = ('') x @hooks; next; } + + # Not a debuild option, so give up. + unshift @ARGV, $arg; + last; + } +} + +if ($save_vars{'PATH'}) { + # Untaint PATH. Very dangerous in general, but anyone running this + # as root can do anything anyway. + $ENV{'PATH'} =~ /^(.*)$/; + $ENV{'PATH'} = $1; +} else { + $ENV{'PATH'} = "/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11"; + $ENV{'PATH'} = join(':', $prepend_path, $ENV{'PATH'}) if $prepend_path; +} +$save_vars{'PATH'} = 1; +$ENV{'TERM'} = 'dumb' unless exists $ENV{'TERM'}; + +# Store a few variables for safe keeping. +my %store_vars; +foreach my $var (( + 'DBUS_SESSION_BUS_ADDRESS', 'DISPLAY', + 'GNOME_KEYRING_SOCKET', 'GPG_AGENT_INFO', + 'SSH_AUTH_SOCK', 'XAUTHORITY' + ) +) { + $store_vars{$var} = $ENV{$var} if defined $ENV{$var}; +} + +unless ($preserve_env) { + foreach my $var (keys %ENV) { + delete $ENV{$var} + unless $save_vars{$var} + or $var =~ /^(LC|DEB)_[A-Z_]+$/ + or $var =~ /^(C(PP|XX)?|LD|F)FLAGS(_APPEND)?$/ + or $var eq 'SOURCE_DATE_EPOCH'; + } +} + +umask 022; + +# Start by duping STDOUT and STDERR +open OLDOUT, ">&", \*STDOUT or fatal "can't dup stdout: $!\n"; +open OLDERR, ">&", \*STDERR or fatal "can't dup stderr: $!\n"; + +# Look for the debian changelog +my $chdir = 0; +until (-r 'debian/changelog') { + $chdir = 1; + chdir '..' or fatal "can't chdir ..: $!"; + if (cwd() eq '/') { + fatal +"cannot find readable debian/changelog anywhere!\nAre you in the source code tree?"; + } +} + +# Find the source package name and version number +my %changelog; +my $c = changelog_parse(); +@changelog{ 'Source', 'Version', 'Distribution' } + = @{$c}{ 'Source', 'Version', 'Distribution' }; + +fatal "no package name in changelog!" + unless exists $changelog{'Source'}; +my $pkg = $changelog{'Source'}; +fatal "no version number in changelog!" + unless exists $changelog{'Version'}; +my $version = $changelog{'Version'}; +(my $sversion = $version) =~ s/^\d+://; +(my $uversion = $sversion) =~ s/-[a-z0-9+\.]+$//i; + +# 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$pkg\\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 $pkg 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 + } +} + +if (!-f "debian/rules") { + my $cwd = cwd(); + fatal +"found debian/changelog in directory\n $cwd\nbut there's no debian/rules there! Are you in the source code tree?"; +} + +if (!-x _ ) { + push @warnings, "Making debian/rules executable!\n"; + chmod 0755, "debian/rules" + or fatal "couldn't make debian/rules executable: $!"; +} + +# Pick up superuser privileges if we are running set[ug]id root +my $uid = $<; +if ($< != 0 && $> == 0) { $< = $> } +my $gid = $(; +if ($( != 0 && $) == 0) { $( = $) } + +# Our first task is to parse the command line options. + +# dpkg-buildpackage variables explicitly initialised in dpkg-buildpackage +my $nosign; +my $forcesign; +my $signsource = $changelog{Distribution} ne 'UNRELEASED'; +my $signchanges = $changelog{Distribution} ne 'UNRELEASED'; +my $signbuildinfo = $changelog{Distribution} ne 'UNRELEASED'; +my $binarytarget = 'binary'; +my $since = ''; +my $usepause = 0; + +# extra dpkg-buildpackage variables not initialised there +my $sourceonly = ''; +my $binaryonly = ''; +my $targetarch = ''; +my $targetgnusystem = ''; + +my $dirn = basename(cwd()); + +# and one for us +my @debsign_opts = (); +# and one for dpkg-buildpackage if needed +my @dpkg_opts = qw(-us -uc -ui); + +my %debuild2dpkg = ( + 'dpkg-buildpackage' => 'init', + 'clean' => 'preclean', + 'dpkg-source' => 'source', + 'build' => 'build', + 'binary' => 'binary', + 'dpkg-genchanges' => 'changes', + 'final-clean' => 'postclean', +); + +for my $h_name (@hooks) { + if (exists $debuild2dpkg{$h_name} && $hook{$h_name}) { + push(@dpkg_opts, + sprintf('--hook-%s=%s', $debuild2dpkg{$h_name}, $hook{$h_name})); + delete $hook{$h_name}; + } +} + +# Parse dpkg-buildpackage options +# First process @dpkg_extra_opts from above + +foreach (@dpkg_extra_opts) { + $_ eq '-h' + and warn "You have a -h option in your configuration file! Ignoring.\n", + next; + /^-r/ and next; # already been processed + /^-p/ and push(@debsign_opts, $_), next; # Key selection options + /^-k/ and push(@debsign_opts, $_), next; # Ditto + /^-[dD]$/ and next; # already been processed + $_ eq '-us' and $signsource = 0, next; + $_ eq '--unsigned-source' and $signsource = 0, next; + $_ eq '-uc' and $signchanges = 0, next; + $_ eq '--unsigned-changes' and $signchanges = 0, next; + $_ eq '-ui' and $signbuildinfo = 0, next; + $_ eq '--unsigned-buildinfo' and $signbuildinfo = 0, next; + $_ eq '--no-sign' and $nosign = 1, next; + $_ eq '--force-sign' and $forcesign = 1, next; + $_ eq '-ap' and $usepause = 1, next; + /^-a(.*)/ and $targetarch = $1, push(@dpkg_opts, $_), next; + $_ eq '-tc' and push(@dpkg_opts, $_), next; + /^-t(.*)/ and $targetgnusystem = $1, push(@dpkg_opts, $_), next; # Ditto + $_ eq '-b' and $binaryonly = $_, $binarytarget = 'binary', + push(@dpkg_opts, $_), next; + $_ eq '-B' and $binaryonly = $_, $binarytarget = 'binary-arch', + push(@dpkg_opts, $_), next; + $_ eq '-A' and $binaryonly = $_, $binarytarget = 'binary-indep', + push(@dpkg_opts, $_), next; + $_ eq '-S' and $sourceonly = $_, push(@dpkg_opts, $_), next; + $_ eq '-F' and $binarytarget = 'binary', push(@dpkg_opts, $_), next; + $_ eq '-G' and $binarytarget = 'binary-arch', push(@dpkg_opts, $_), next; + $_ eq '-g' and $binarytarget = 'binary-indep', push(@dpkg_opts, $_), next; + + if (/^--build=(.*)$/) { + my $argstr = $_; + my @builds = split(/,/, $1); + my ($binary, $source); + for my $build (@builds) { + if ($build =~ m/^(?:binary|full)$/) { + $source++ if $1 eq 'full'; + $binary++; + $binarytarget = 'binary'; + } elsif ($build eq 'any') { + $binary++; + $binarytarget = 'binary-arch'; + } elsif ($build eq 'all') { + $binary++; + $binarytarget = 'binary-indep'; + } + } + $binaryonly = (!$source && $binary); + $sourceonly = ($source && !$binary); + push(@dpkg_opts, $argstr); + } + /^-v(.*)/ and $since = $1, push(@dpkg_opts, $_), next; + /^-m(.*)/ and push(@debsign_opts, $_), push(@dpkg_opts, $_), next; + /^-e(.*)/ and push(@debsign_opts, $_), push(@dpkg_opts, $_), next; + push(@dpkg_opts, $_); +} + +while ($_ = shift) { + $_ eq '-h' and usage(), exit 0; + /^-r(.*)/ and $root_command = $1, next; + /^-p/ and push(@debsign_opts, $_), next; # Key selection options + /^-k/ and push(@debsign_opts, $_), next; # Ditto + $_ eq '-us' and $signsource = 0, next; + $_ eq '--unsigned-source' and $signsource = 0, next; + $_ eq '-uc' and $signchanges = 0, next; + $_ eq '--unsigned-changes' and $signchanges = 0, next; + $_ eq '-ui' and $signbuildinfo = 0, next; + $_ eq '--unsigned-buildinfo' and $signbuildinfo = 0, next; + $_ eq '--no-sign' and $nosign = 1, next; + $_ eq '--force-sign' and $forcesign = 1, next; + $_ eq '-ap' and $usepause = 1, next; + /^-a(.*)/ and $targetarch = $1, push(@dpkg_opts, $_), next; + $_ eq '-tc' and push(@dpkg_opts, $_), next; + /^-t(.*)/ and $targetgnusystem = $1, next; + $_ eq '-b' and $binaryonly = $_, $binarytarget = 'binary', + push(@dpkg_opts, $_), next; + $_ eq '-B' and $binaryonly = $_, $binarytarget = 'binary-arch', + push(@dpkg_opts, $_), next; + $_ eq '-A' and $binaryonly = $_, $binarytarget = 'binary-indep', + push(@dpkg_opts, $_), next; + $_ eq '-S' and $sourceonly = $_, push(@dpkg_opts, $_), next; + $_ eq '-F' and $binarytarget = 'binary', push(@dpkg_opts, $_), next; + $_ eq '-G' and $binarytarget = 'binary-arch', push(@dpkg_opts, $_), next; + $_ eq '-g' and $binarytarget = 'binary-indep', push(@dpkg_opts, $_), next; + + if (/^--build=(.*)$/) { + my $argstr = $_; + my @builds = split(/,/, $1); + my ($binary, $source); + for my $build (@builds) { + if ($build eq 'full') { + $source++; + $binary++; + $binarytarget = 'binary'; + } elsif ($build eq 'binary') { + $binary++; + $binarytarget = 'binary'; + } elsif ($build eq 'any') { + $binary++; + $binarytarget = 'binary-arch'; + } elsif ($build eq 'all') { + $binary++; + $binarytarget = 'binary-indep'; + } + } + $binaryonly = (!$source && $binary); + $sourceonly = ($source && !$binary); + push(@dpkg_opts, $argstr); + } + /^-v(.*)/ and $since = $1, push(@dpkg_opts, $_), next; + /^-m(.*)/ and push(@debsign_opts, $_), push(@dpkg_opts, $_), next; + /^-e(.*)/ and push(@debsign_opts, $_), push(@dpkg_opts, $_), next; + + # these non-dpkg-buildpackage options make us stop + if ($_ eq '--lintian-opts') { + unshift @ARGV, $_; + last; + } + if ($_ eq '--') { + last; + } + push(@dpkg_opts, $_); +} + +# Pick up lintian options if necessary +if (@ARGV) { + # Check that option is sensible + if ($ARGV[0] eq '--lintian-opts') { + if (!$run_lintian) { + push @warnings, "$ARGV[0] option given but not running lintian!"; + } + shift; + push(@lintian_opts, @ARGV); + undef @ARGV; + } +} + +if ($nosign) { + $signchanges = 0; + $signsource = 0; + $signbuildinfo = 0; +} + +if ($forcesign) { + $signchanges = 1; + $signsource = 1; + $signbuildinfo = 1; +} + +if ($signchanges == 1 and $signsource == 0) { + push @warnings, "Setting -us without setting -uc, signing .dsc anyway\n"; +} + +if ($signchanges == 1 and $signbuildinfo == 0) { + push @warnings, + "Setting -ui without setting -uc, signing .buildinfo anyway\n"; +} + +# Next dpkg-buildpackage steps: +# mustsetvar package/version have been done above; we've called the +# results $pkg and $version +# mustsetvar maintainer is only needed for signing, so we leave that +# to debsign or dpkg-sig +# Call to dpkg-architecture to set DEB_{BUILD,HOST}_* environment +# variables +my @dpkgarch = 'dpkg-architecture'; +if ($targetarch) { + push @dpkgarch, "-a${targetarch}"; +} +if ($targetgnusystem) { + push @dpkgarch, "-t${targetgnusystem}"; +} +push @dpkgarch, '-f'; + +my $archinfo; +spawn( + exec => [@dpkgarch], + to_string => \$archinfo, + wait_child => 1 +); +foreach (split /\n/, $archinfo) { + /^(.*)=(.*)$/ and $ENV{$1} = $2; +} + +# We need to do the arch, pv, pva stuff to figure out +# what the changes file will be called, +my ($arch, $dsc, $changes, $build); +if ($sourceonly) { + $arch = 'source'; +} elsif ($binarytarget eq 'binary-indep') { + $arch = 'all'; +} else { + $arch = $ENV{DEB_HOST_ARCH}; +} + +# Handle dpkg source format "3.0 (git)" packages (no tarballs) +if (-r "debian/source/format") { + open FMT, "debian/source/format" or die $!; + my $srcfmt = <FMT>; + close FMT; + chomp $srcfmt; + if ($srcfmt eq "3.0 (git)") { $tgz_check = 0; } +} + +$dsc = "${pkg}_${sversion}.dsc"; +my $orig_prefix = "${pkg}_${uversion}.orig.tar"; +my $origdir = basename(cwd()) . ".orig"; +if ( !$binaryonly + and $tgz_check + and $uversion ne $sversion + and !-f "../${orig_prefix}.bz2" + and !-f "../${orig_prefix}.lzma" + and !-f "../${orig_prefix}.gz" + and !-f "../${orig_prefix}.xz" + and !-d "../$origdir") { + print STDERR "This package has a Debian revision number but there does" + . " not seem to be\nan appropriate original tar file or .orig" + . " directory in the parent directory;\n(expected one of" + . " ${orig_prefix}.gz, ${orig_prefix}.bz2,\n${orig_prefix}.lzma, " + . " ${orig_prefix}.xz or $origdir)\ncontinue anyway? (y/n) "; + my $ans = <STDIN>; + exit 1 unless $ans =~ /^y/i; +} + +# Convert debuild-specific _APPEND variables to those recognized by +# dpkg-buildpackage +my @buildflags = qw(CPPFLAGS CFLAGS CXXFLAGS FFLAGS LDFLAGS); +foreach my $flag (@buildflags) { + if (exists $ENV{"${flag}_APPEND"}) { + $ENV{"DEB_${flag}_APPEND"} = delete $ENV{"${flag}_APPEND"}; + } +} + +if (defined($checkbuilddep)) { + unshift @dpkg_opts, ($checkbuilddep ? "-D" : "-d"); +} +unshift @dpkg_opts, "-r$root_command" if $root_command; + +if (@ARGV) { + # Run each rule + for my $target (@ARGV) { + system_withecho('dpkg-buildpackage', '--rules-target', $target, + @dpkg_opts); + } + + # Any warnings? + if (@warnings) { + # Don't know why we need this, but seems that we do, otherwise, + # the warnings get muddled up with the other output. + IO::Handle::flush(\*STDOUT); + + my $warns = @warnings > 1 ? "S" : ""; + warn "\nWARNING$warns generated by $progname:\n" + . join("\n", @warnings) . "\n"; + } +} else { + if ($run_lintian && system('command -v lintian >/dev/null 2>&1') == 0) { + $lintian_exists = 1; + } + # We'll need to be a bit cleverer to determine the changes file name; + # see below + $build = "${pkg}_${sversion}_${arch}.build"; + $changes = "${pkg}_${sversion}_${arch}.changes"; + open BUILD, "| tee ../$build" or fatal "couldn't open pipe to tee: $!"; + $logging = 1; + close STDOUT; + close STDERR; + open STDOUT, ">&BUILD" or fatal "can't reopen stdout: $!"; + open STDERR, ">&BUILD" or fatal "can't reopen stderr: $!"; + + system_withecho('dpkg-buildpackage', @dpkg_opts); + + chdir '..' or fatal "can't chdir: $!"; + + open CHANGES, '<', $changes or fatal "can't open $changes for reading: $!"; + my @changefilecontents = <CHANGES>; + close CHANGES; + + # check Ubuntu merge Policy: When merging with Debian, -v must be used + # and the remaining changes described + my $ch = join "\n", @changefilecontents; + if ( $sourceonly + && $version =~ /ubuntu1$/ + && $ENV{'DEBEMAIL'} =~ /ubuntu/ + && $ch =~ /(merge|sync).*Debian/i) { + push(@warnings, +"Ubuntu merge policy: when merging Ubuntu packages with Debian, -v must be used" + ) unless $since; + push(@warnings, +"Ubuntu merge policy: when merging Ubuntu packages with Debian, changelog must describe the remaining Ubuntu changes" + ) + unless $ch + =~ /Changes:.*(remaining|Ubuntu)(.|\n )*(differen|changes)/is; + } + + run_hook('lintian', $run_lintian && $lintian_exists); + + if ($run_lintian && $lintian_exists) { + $< = $> = $uid; # Give up on root privileges if we can + $( = $) = $gid; + my @lintian + = ('lintian', @lintian_extra_opts, @lintian_opts, $changes); + print "Now running @lintian ...\n"; + system(@lintian); + print "Finished running lintian.\n"; + } + + # They've insisted. Who knows why?! + if (($signchanges or $signsource) and $usepause) { + print "Press the return key to start signing process\n"; + <STDIN>; + } + + run_hook('signing', ($signchanges || (!$sourceonly and $signsource))); + + if ($signchanges) { + foreach my $var (keys %store_vars) { + $ENV{$var} = $store_vars{$var}; + } + print "Now signing changes and any dsc files...\n"; + if ($username) { + system('debrsign', @debsign_opts, $username, $changes) == 0 + or fatal "running debrsign failed"; + } else { + system('debsign', @debsign_opts, $changes) == 0 + or fatal "running debsign failed"; + } + } elsif (!$sourceonly and $signsource) { + print "Now signing dsc file...\n"; + if ($username) { + system('debrsign', @debsign_opts, $username, $dsc) == 0 + or fatal "running debrsign failed"; + } else { + system('debsign', @debsign_opts, $dsc) == 0 + or fatal "running debsign failed"; + } + } + + run_hook('post-dpkg-buildpackage', 1); + + # Any warnings? + if (@warnings) { + # Don't know why we need this, but seems that we do, otherwise, + # the warnings get muddled up with the other output. + IO::Handle::flush(\*STDOUT); + + my $warns = @warnings > 1 ? "S" : ""; + warn "\nWARNING$warns generated by $progname:\n" + . join("\n", @warnings) . "\n"; + } + # close the logging process + close STDOUT; + close STDERR; + close BUILD; + open STDOUT, ">&", \*OLDOUT; + open STDERR, ">&", \*OLDERR; +} +exit 0; + +###### Subroutines + +sub setDebuildHook() { + my ($name, $val) = @_; + + unless (grep /^$name$/, @hooks) { + fatal + "unknown hook $name,\nrun $progname --help for usage information"; + } + + if ($externalHook{$name} && $dpkgHook{$name} && $val) { + $hook{$name} = 'cd ..; ' . $val; + } else { + $hook{$name} = $val; + } +} + +sub setDpkgHook() { + my ($name, $val) = @_; + + unless (grep /^$name$/, @hooks) { + fatal + "unknown hook $name,\nrun $progname --help for usage information"; + } + + if ($externalHook{$name} && !$dpkgHook{$name} && $val) { + $hook{$name} = 'cd ..; ' . $val; + } else { + $hook{$name} = $val; + } +} + +sub system_withecho(@) { + print STDERR " ", join(" ", @_), "\n"; + system(@_); + if ($? >> 8) { + fatal "@_ failed"; + } +} + +sub run_hook ($$) { + my ($hook, $act) = @_; + return unless $hook{$hook}; + + print STDERR " Running $hook-hook\n"; + my $hookcmd = $hook{$hook}; + $act = $act ? 1 : 0; + my %per = ( + "%" => "%", + "p" => $pkg, + "v" => $version, + "s" => $sversion, + "u" => $uversion, + "a" => $act + ); + $hookcmd =~ s/\%(.)/exists $per{$1} ? $per{$1} : + (warn ("Unrecognised \% substitution in hook: \%$1\n"), "\%$1")/eg; + + system_withecho($hookcmd); + + if ($? >> 8) { + warn "$progname: $hook-hook failed\n"; + exit($? >> 8); + } +} + +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/; + # redirect stderr before we die... + if ($logging) { + close STDOUT; + close STDERR; + close BUILD; + open STDOUT, ">&", \*OLDOUT; + open STDERR, ">&", \*OLDERR; + } + die $msg; +} diff --git a/scripts/dep3changelog.1 b/scripts/dep3changelog.1 new file mode 100644 index 0000000..1e5eb9b --- /dev/null +++ b/scripts/dep3changelog.1 @@ -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) diff --git a/scripts/dep3changelog.pl b/scripts/dep3changelog.pl new file mode 100755 index 0000000..4c003c8 --- /dev/null +++ b/scripts/dep3changelog.pl @@ -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}); + } + } +} diff --git a/scripts/desktop2menu.pl b/scripts/desktop2menu.pl new file mode 100755 index 0000000..195410b --- /dev/null +++ b/scripts/desktop2menu.pl @@ -0,0 +1,317 @@ +#!/usr/bin/perl + +# desktop2menu: This program generates a skeleton menu file from a +# freedesktop.org desktop file +# +# Written by Sune Vuorela <debian@pusling.com> +# Modifications by Adam D. Barratt <adam@adam-barratt.org.uk> +# Copyright 2007 Sune Vuorela <debian@pusling.com> +# Modifications Copyright 2007 Adam D. Barratt <adam@adam-barratt.org.uk> +# 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 + +desktop2menu - create a menu file skeleton from a desktop file + +=head1 SYNOPSIS + +B<desktop2menu> B<--help>|B<--version> + +B<desktop2menu> I<desktop file> [I<package name>] + +=head1 DESCRIPTION + +B<desktop2menu> generates a skeleton menu file from the supplied +freedesktop.org desktop file. + +The package name to be used in the menu file may be passed as an additional +argument. If it is not supplied then B<desktop2menu> will attempt to derive +the package name from the data in the desktop file. + +=head1 LICENSE + +This program is Copyright (C) 2007 by Sune Vuorela <debian@pusling.com>. It +was modified by Adam D. Barratt <adam@adam-barratt.org.uk> for the devscripts +package. +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 + +Sune Vuorela <debian@pusling.com> with modifications by Adam D. Barratt +<adam@adam-barratt.org.uk> + +=cut + +use warnings; +use strict; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use File::Basename; + +my $progname = basename($0); + +BEGIN { + pop @INC if $INC[-1] eq '.'; + # Load the File::DesktopEntry module safely + eval { require File::DesktopEntry; }; + if ($@) { + my $progname = basename $0; + if ($@ =~ /^Can\'t locate File\/DesktopEntry\.pm/) { + die +"$progname: you must have the libfile-desktopentry-perl package installed\nto use this script\n"; + } + die +"$progname: problem loading the File::DesktopEntry module:\n $@\nHave you installed the libfile-desktopentry-perl package?\n"; + } + import File::DesktopEntry; +} + +use File::DesktopEntry; + +# Big generic mapping between fdo sections and menu sections +my %mappings = ( + "AudioVideo" => "Applications/Video", + "Audio" => "Applications/Sound", + "Video" => "Applications/Video", + "Development" => "Applications/Programming", + "Education" => "Applications/Education", + "Game" => "Games!WARN", + "Graphics" => "Applications/Graphics!WARN", + "Network" => "Applications/Network!WARN", + "Office" => "Applications/Office", + "System" => "Applications/System/Administration", + "Utility" => "Applications!WARN", + "Building" => "Applications/Programming", + "Debugger" => "Applications/Programming", + "IDE" => "Applications/Programming", + "Profiling" => "Applications/Programming", + "RevisionControl" => "Applications/Programming", + "Translation" => "Applications/Programming", + "Calendar" => "Applications/Data Management", + "ContactManagement" => "Applications/Data Management", + "Database" => "Applications/Data Management", + "Dictionary" => "Applications/Text", + "Chart" => "Applications/Office", + "Email" => "Applications/Network/Communication", + "Finance" => "Applications/Office", + "FlowChart" => "Applications/Office", + "PDA" => "Applications/Mobile Devices", + "ProjectManagement" => "Applications/Project Management", + "Presentation" => "Applications/Office", + "Spreadsheet" => "Applications/Office", + "Wordprocessor" => "Applications/Office", + "2DGraphics" => "Applications/Graphics", + "VectorGraphics" => "Applications/Graphics", + "RasterGraphics" => "Applications/Graphics", + "3DGraphics" => "Applications/Graphics", + "Scanning" => "Applications/Graphics", + "OCR" => "Applications/Text", + "Photography" => "Applications/Graphics", + "Publishing" => "Applications/Office", + "Viewer" => "Applications/Viewers", + "TextTools" => "Applications/Text", + "DesktopSettings" => "Applications/System/Administration", + "HardwareSettings" => "Applications/System/Hardware", + "Printing" => "Applications/System/Administration", + "PackageManager" => "Applications/System/Package Management", + "Dialup" => "Applications/System/Administration", + "InstantMesasging" => "Applications/Network/Communication", + "Chat" => "Applications/Network/Communication", + "IRCClient" => "Applications/Network/Communication", + "FileTransfer" => "Applications/Network/File Transfer", + "HamRadio" => "Applications/Amateur Radio", + "News" => "Applications/Network/Web News", + "P2P" => "Applications/File Transfer", + "RemoteAccess" => "Applications/System/Administration", + "Telephony" => "Applications/Network/Communication", + "TelephonyTools" => "Applications/Network/Communication", + "VideoConference" => "Applications/Network/Communication", + "Midi" => "Applications/Sound", + "Mixer" => "Applications/Sound", + "Sequencer" => "Applications/Sound", + "Tuner" => "Applications/TV and Radio", + "TV" => "Applications/TV and Radio", + "AudioVideoEditing" => "Applications/Video!WARN", + "Player" => "Applications/Video!WARN", + "Recorder" => "Applications/Video!WARN", + "DiscBurning" => "Applications/File Management", + "ActionGame" => "Games/Action", + "AdventureGame" => "Games/Adventure", + "ArcadeGame" => "Games/Action", + "BoardGame" => "Games/Board", + "BlocksGame" => "Games/Blocks", + "CardGame" => "Games/Card", + "KidsGames" => "Games/Toys!WARN", + "LogicGames" => "Games/Puzzles", + "RolePlaying" => "Games/Adventure", + "Simulation" => "Games/Simulation", + "SportsGame" => "Games/Action", + "StrategyGame" => "Games/Strategy", + "Art" => "Applications/Education", + "Construction" => "Applications/Education", + "Music" => "Applications/Education", + "Languages" => "Applications/Education", + "Science" => "Applications/Science!WARN", + "ArtificialIntelligence" => "Applications/Science!WARN", + "Astronomy" => "Applications/Science/Astronomy", + "Biology" => "Applications/Science/Biology", + "Chemistry" => "Applications/Science/Chemistry", + "ComputerScience" => "Applications/Science/Electronics!WARN", + "DataVisualization" => "Applications/Science/Data Analysis", + "Economy" => "Applications/Office", + "Electricity" => "Applications/Science/Engineering", + "Geography" => "Applications/Science/Geoscience", + "Geology" => "Applications/Science/Geoscience", + "Geoscience" => "Applications/Science/Geoscience", + "History" => "Applications/Science/Social", + "ImageProcessing" => "Applications/Graphics", + "Literature" => "Applications/Data Management", + "Math" => "Applications/Science/Mathematics", + "NumericalAnalyzisis" => "Applications/Science/Mathematics", + "MedicalSoftware" => "Applications/Science/Medicine", + "Physics" => "Applications/Science/Physics", + "Robotics" => "Applications/Science/Engineering", + "Sports" => "Games/Tools!WARN", + "ParallelComputing" => "Applications/Science/Electronics!WARN", + "Amusement" => "Games/Toys", + "Archiving" => "Applications/File Management", + "Compression" => "Applications/File Management", + "Electronics" => "Applications/Science/Electronics", + "Emulator" => "Applications/Emulators", + "Engineering" => "Applications/Science/Engineering", + "FileTools" => "Applications/File Management", + "FileManager" => "Applications/File Management", + "TerminalEmulator" => "Applications/Shells", + "Filesystem" => "Applications/System/Administration", + "Monitor" => "Applications/System/Monitoring", + "Security" => "Applications/System/Security", + "Accessibility" => "Applications/Accessibility", + "Calculator" => "Applications/Science/Mathematics", + "Clock" => "Games/Toys", + "TextEditor" => "Applications/Editors", +); + +#values mentioned in Categories we accept as valid hints. +my %hintscategories = ( + "KDE" => "true", + "Qt" => "true", + "GNOME" => "true", + "GTK" => "true", +); + +my ($opt_help, $opt_version); + +GetOptions( + "help|h" => \$opt_help, + "version" => \$opt_version, + ) + or die +"Usage: $progname desktopfile packagename\nRun $progname --help for more details\n"; + +if ($opt_help) { help(); exit 0; } +if ($opt_version) { version(); exit 0; } + +if (@ARGV == 0) { + help(); + exit 0; +} + +my $section; +my @hints; +my $needs; +my $warnings = 0; + +my $filename = shift @ARGV; +my $file = File::DesktopEntry->new_from_file("$filename"); + +# do menu files for non-applications make sense? +die $file->get_value('Name') . " isn't an application\n" + unless $file->get_value('Type') eq 'Application'; + +my $package = join(' ', @ARGV); +if (!$package) { + # Bad guess, but... maybe icon name could be better? + $package = $file->get_value('Name'); + print STDERR + "WARNING: Package not specified. Guessing package as: $package\n"; + $warnings++; +} + +my $category = $file->get_value('Categories'); + +my @categories = reverse split(";", $category); +foreach (@categories) { + if ($mappings{$_} && !$section) { + $section = $mappings{$_}; + } + if ($hintscategories{$_}) { + push(@hints, $_); + } +} + +die "Desktop file has invalid categories" unless $section; + +# Not all mappings are completely accurate. Most are, but... +if ($section =~ /!WARN/) { + print STDERR + "WARNING: Section is highly inaccurate. Please check it manually\n"; + $warnings++; +} + +# Let's just pretend that the wm and the vc needs don't exist. +if ($category =~ /ConsoleOnly/) { + $needs = "text"; +} else { + $needs = "X11"; +} + +print "\n" if $warnings > 0; +print "?package(" . $package . "): \\\n"; +print "\tneeds=\"" . $needs . "\" \\\n"; +print "\tsection=\"" . $section . "\" \\\n"; +print "\ttitle=\"" . $file->get_value('Name') . "\" \\\n"; +print "\thints=\"" . join(",", @hints) . "\" \\\n" if @hints; +print "\tcommand=\"" . $file->get_value('Exec') . "\" \\\n"; +print "\ticon=\"/usr/share/pixmaps/" + . $file->get_value('Icon') + . ".xpm\" \\\n"; +print "\n"; + +# Unnecessary. but for clarity +exit 0; + +sub help { + print <<"EOF"; +Usage: $progname [options] filename packagename + +Valid options are: + --help, -h Display this message + --version, -v Display version and copyright info +EOF +} + +sub version { + print <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +Copyright (C) 2007 by Sune Vuorela <debian\@pusling.com>. +Modifications copyright (C) 2007 by Adam D. Barratt <adam\@adam-barratt.org.uk> + +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 +} diff --git a/scripts/devscripts/control.py b/scripts/devscripts/control.py new file mode 100644 index 0000000..d53b4a3 --- /dev/null +++ b/scripts/devscripts/control.py @@ -0,0 +1,308 @@ +# 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") + + 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, + ) diff --git a/scripts/devscripts/logger.py b/scripts/devscripts/logger.py new file mode 100644 index 0000000..f99de37 --- /dev/null +++ b/scripts/devscripts/logger.py @@ -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 diff --git a/scripts/devscripts/test/__init__.py b/scripts/devscripts/test/__init__.py new file mode 100644 index 0000000..59d2920 --- /dev/null +++ b/scripts/devscripts/test/__init__.py @@ -0,0 +1,67 @@ +# 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 + +SCRIPTS = [ + "debbisect", + "debdiff-apply", + "debootsnap", + "deb-janitor", + "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 diff --git a/scripts/devscripts/test/pylint.conf b/scripts/devscripts/test/pylint.conf new file mode 100644 index 0000000..888884e --- /dev/null +++ b/scripts/devscripts/test/pylint.conf @@ -0,0 +1,67 @@ +[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 diff --git a/scripts/devscripts/test/test_black.py b/scripts/devscripts/test/test_black.py new file mode 100644 index 0000000..565018f --- /dev/null +++ b/scripts/devscripts/test/test_black.py @@ -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]) <= 20: + self.skipTest("black >= 21 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()}" + ) diff --git a/scripts/devscripts/test/test_control.py b/scripts/devscripts/test/test_control.py new file mode 100644 index 0000000..cb6aa0b --- /dev/null +++ b/scripts/devscripts/test/test_control.py @@ -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) diff --git a/scripts/devscripts/test/test_debootsnap.py b/scripts/devscripts/test/test_debootsnap.py new file mode 100644 index 0000000..c1e71fd --- /dev/null +++ b/scripts/devscripts/test/test_debootsnap.py @@ -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") diff --git a/scripts/devscripts/test/test_flake8.py b/scripts/devscripts/test/test_flake8.py new file mode 100644 index 0000000..9781a5d --- /dev/null +++ b/scripts/devscripts/test/test_flake8.py @@ -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)) diff --git a/scripts/devscripts/test/test_help.py b/scripts/devscripts/test/test_help.py new file mode 100644 index 0000000..39335f8 --- /dev/null +++ b/scripts/devscripts/test/test_help.py @@ -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 diff --git a/scripts/devscripts/test/test_isort.py b/scripts/devscripts/test/test_isort.py new file mode 100644 index 0000000..4190859 --- /dev/null +++ b/scripts/devscripts/test/test_isort.py @@ -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()}" + ) diff --git a/scripts/devscripts/test/test_logger.py b/scripts/devscripts/test/test_logger.py new file mode 100644 index 0000000..e6322ea --- /dev/null +++ b/scripts/devscripts/test/test_logger.py @@ -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(), "") diff --git a/scripts/devscripts/test/test_pylint.py b/scripts/devscripts/test/test_pylint.py new file mode 100644 index 0000000..35c0162 --- /dev/null +++ b/scripts/devscripts/test/test_pylint.py @@ -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)) diff --git a/scripts/devscripts/test/test_suspicious_source.py b/scripts/devscripts/test/test_suspicious_source.py new file mode 100644 index 0000000..bb61156 --- /dev/null +++ b/scripts/devscripts/test/test_suspicious_source.py @@ -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), "") diff --git a/scripts/dget.pl b/scripts/dget.pl new file mode 100755 index 0000000..1149f6e --- /dev/null +++ b/scripts/dget.pl @@ -0,0 +1,745 @@ +#!/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 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 (system("command -v curl >/dev/null 2>&1") == 0) { + $wget = "curl"; +} elsif (system("command -v wget >/dev/null 2>&1") == 0) { + $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). +It is capable of downloading several packages 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; +} + +# we reinvent "apt-get -d install" here, without requiring root +# (and we do not download dependencies) +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$/; + push @sources, "/etc/apt/sources.list.d/$_"; + } + + foreach my $source (@sources) { + $apt = IO::File->new($source) or die "$source: $!"; + while (<$apt>) { + if (/^\s*deb\s*(?:\[[^]]*\]\s*)?($host_re\b)/) { + $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 /, /, $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). Unlike B<apt-get install -d>, it does not +require root privileges, writes to the current directory, and does not +download dependencies. 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> was written to make it easier to retrieve source packages from +the web for sponsor uploads. For checking the package with +B<debdiff>, the last binary version is available via B<dget> +I<package>, the last source version via B<apt-get source> I<package>. + +=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 I<.deb> files for the previous version of a package and run B<debdiff> +on them: + + dget --all mypackage=1.2-1 + debdiff --from *_1.2-1_*.deb --to *_1.2-2_*.deb + +=head1 BUGS AND COMPATIBILITY + +B<dget> I<package> should be implemented in B<apt-get install -d>. + +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) diff --git a/scripts/diff2patches.1 b/scripts/diff2patches.1 new file mode 100644 index 0000000..f474dc3 --- /dev/null +++ b/scripts/diff2patches.1 @@ -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> diff --git a/scripts/diff2patches.sh b/scripts/diff2patches.sh new file mode 100755 index 0000000..fa5fd6a --- /dev/null +++ b/scripts/diff2patches.sh @@ -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 diff --git a/scripts/dpkg-depcheck.1 b/scripts/dpkg-depcheck.1 new file mode 100644 index 0000000..7a546a5 --- /dev/null +++ b/scripts/dpkg-depcheck.1 @@ -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. diff --git a/scripts/dpkg-depcheck.pl b/scripts/dpkg-depcheck.pl new file mode 100755 index 0000000..afa2d75 --- /dev/null +++ b/scripts/dpkg-depcheck.pl @@ -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; +} diff --git a/scripts/dpkg-genbuilddeps.1 b/scripts/dpkg-genbuilddeps.1 new file mode 100644 index 0000000..7a7f5cd --- /dev/null +++ b/scripts/dpkg-genbuilddeps.1 @@ -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>. diff --git a/scripts/dpkg-genbuilddeps.sh b/scripts/dpkg-genbuilddeps.sh new file mode 100755 index 0000000..04cb048 --- /dev/null +++ b/scripts/dpkg-genbuilddeps.sh @@ -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 "$@" diff --git a/scripts/dscextract.1 b/scripts/dscextract.1 new file mode 100644 index 0000000..d455c43 --- /dev/null +++ b/scripts/dscextract.1 @@ -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>. diff --git a/scripts/dscextract.bash_completion b/scripts/dscextract.bash_completion new file mode 100644 index 0000000..bf7eadc --- /dev/null +++ b/scripts/dscextract.bash_completion @@ -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 : diff --git a/scripts/dscextract.sh b/scripts/dscextract.sh new file mode 100755 index 0000000..78e2334 --- /dev/null +++ b/scripts/dscextract.sh @@ -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 diff --git a/scripts/dscverify.1 b/scripts/dscverify.1 new file mode 100644 index 0000000..5f065f3 --- /dev/null +++ b/scripts/dscverify.1 @@ -0,0 +1,86 @@ +.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 +package. (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-maintainers.gpg + +- /usr/share/keyrings/debian-nonupload.gpg +.SH "SEE ALSO" +.BR gpg (1), +.BR gpg2 (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>. diff --git a/scripts/dscverify.bash_completion b/scripts/dscverify.bash_completion new file mode 100644 index 0000000..789a556 --- /dev/null +++ b/scripts/dscverify.bash_completion @@ -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 : diff --git a/scripts/dscverify.pl b/scripts/dscverify.pl new file mode 100755 index 0000000..0916646 --- /dev/null +++ b/scripts/dscverify.pl @@ -0,0 +1,457 @@ +#!/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 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 { !system('sh', '-c', "command -v $_ >/dev/null 2>&1") } + qw(gpg2 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-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; diff --git a/scripts/edit-patch.sh b/scripts/edit-patch.sh new file mode 100755 index 0000000..8adfb67 --- /dev/null +++ b/scripts/edit-patch.sh @@ -0,0 +1,313 @@ +#!/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() { + CDBS_PATCHSYS="^[^#]*simple-patchsys.mk" + + if grep -q "$CDBS_PATCHSYS" debian/rules; then + PATCHSYSTEM="cdbs" + require_installed cdbs-edit-patch "no cdbs-edit-patch found, is 'cdbs' installed?" + elif [ -e debian/patches/00list ]; then + PATCHSYSTEM="dpatch" + require_installed dpatch-edit-patch "no dpatch-edit-patch found, is 'dpatch' installed?" + elif [ -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: +# - dpatch ends with .dpatch +# - cdbs/quilt with .patch +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%.*} + if [ "$PATCHSYSTEM" = "quilt" ]; then + PATCHNAME="${PATCHNAME}.patch" + elif [ "$PATCHSYSTEM" = "cdbs" ]; then + PATCHNAME="${PATCHNAME}.patch" + elif [ "$PATCHSYSTEM" = "dpatch" ]; then + PATCHNAME="${PATCHNAME}.dpatch" + elif [ "$PATCHSYSTEM" = "none" ]; then + PATCHNAME="${PATCHNAME}.patch" + fi + + echo "Normalizing patch name to $PATCHNAME" +} + +edit_patch_cdbs() { + cdbs-edit-patch $PATCHNAME + vcs_add debian/patches/$1 +} + +edit_patch_dpatch() { + dpatch-edit-patch $PATCHNAME + # add if needed + if ! grep -q $1 $PREFIX/00list; then + echo "$1" >> $PREFIX/00list + fi + vcs_add $PREFIX/00list $PREFIX/$1 +} + +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_cdbs() { + # $1 is the original patchfile, $2 the normalized name + cp $1 $PREFIX/$2 + vcs_add $PREFIX/$2 +} + +add_patch_dpatch() { + # $1 is the original patchfile, $2 the normalized name + cp $1 $PREFIX + if ! grep -q $2 $PREFIX/00list; then + echo "$2" >> $PREFIX/00list + fi + vcs_add $PREFIX/$2 $PREFIX/00list +} + +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 dpatch/quilt/cdbs?)" + 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 + elif [ "$PATCHSYSTEM" = "dpatch" ]; then + echo "$PATCHNAME" >> $PREFIX/00list + + # Add the dpatch header to files that don't already have it + if ! grep -q "@DPATCH@" "$PREFIX/$PATCHNAME"; then + sed -i '1i#! /bin/sh /usr/share/dpatch/dpatch-run\n@DPATCH@' $PREFIX/$PATCHNAME + fi + 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 +# - dbs patch system + +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 $@ diff --git a/scripts/getbuildlog.1 b/scripts/getbuildlog.1 new file mode 100644 index 0000000..e6cc228 --- /dev/null +++ b/scripts/getbuildlog.1 @@ -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>. diff --git a/scripts/getbuildlog.sh b/scripts/getbuildlog.sh new file mode 100755 index 0000000..172aa13 --- /dev/null +++ b/scripts/getbuildlog.sh @@ -0,0 +1,151 @@ +#!/bin/sh +# +# getbuildlog: download package build logs from Debian auto-builders +# +# Copyright © 2008 Frank S. Thomas <fst@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 -e + +PROGNAME=${0##*/} + +usage() { + cat <<EOT +Usage: $PROGNAME <package> [<version-pattern>] [<architecture-pattern>] + Downloads build logs of <package> from Debian auto-builders. + If <version-pattern> or <architecture-pattern> are given, only build logs + whose versions and architectures, respectively, matches the given patterns + are downloaded. + + If <version-pattern> is "last" then only the logs for the most recent + version of <package> found on buildd.debian.org will be downloaded. + + If <version-pattern> is "last-all" then the logs for the most recent + version found on each build log index will be downloaded. +Options: + -h, --help Show this help message. + -V, --version Show version and copyright information. +Examples: + # Download amd64 build log for hello version 2.2-1: + $PROGNAME hello 2\.2-1 amd64 + + # Download mips(el) build logs of all glibc versions: + $PROGNAME glibc "" mips.* + + # Download all build logs of backported wesnoth versions: + $PROGNAME wesnoth .*bpo.* +EOT +} + +version() { + cat <<EOT +This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This code is copyright 2008 by Frank S. Thomas, 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. +EOT +} + +[ "$1" = "-h" ] || [ "$1" = "--help" ] && usage && exit 0 +[ "$1" = "-V" ] || [ "$1" = "--version" ] && version && exit 0 + +[ $# -ge 1 ] && [ $# -le 3 ] || { usage && exit 1; } + +if ! which wget > /dev/null 2>&1; then + echo "$PROGNAME: this program requires the wget package to be installed"; + exit 1 +fi + +PACKAGE=$1 +VERSION=${2:-[:~+.[:alnum:]-]+} +ARCH=${3:-[[:alnum:]-]+} +ESCAPED_PACKAGE=$(echo "$PACKAGE" | sed -e 's/\+/\\\+/g') + +GET_LAST_VERSION=no +if [ "$VERSION" = "last" ]; then + GET_LAST_VERSION=yes + VERSION=[:~+.[:alnum:]-]+ +elif [ "$VERSION" = "last-all" ]; then + GET_LAST_VERSION=all + VERSION=[:~+.[:alnum:]-]+ +fi + +PATTERN="fetch\.(cgi|php)\?pkg=$ESCAPED_PACKAGE&arch=$ARCH&ver=$VERSION&\ +stamp=[[:digit:]]+" + +getbuildlog() { + BASE=$1 + ALL_LOGS=$(mktemp --tmpdir getbuildlog.tmp.XXXXXXXXXX) + + trap 'rm -f "$ALL_LOGS"' EXIT + + wget -q -O $ALL_LOGS "$BASE/status/logs.php?pkg=$PACKAGE" + + # Put each href in $ALL_LOGS on a separate line so that $PATTERN + # matches only one href. This is required because grep is greedy. + sed -i -e "s/href=\"/\nhref=\"/g" $ALL_LOGS + # Quick-and-dirty unescaping + sed -i -e "s/&/\&/g" -e "s/%2B/\+/g" -e "s/%3A/:/g" -e "s/%7E/~/g" $ALL_LOGS + + # If only the last version was requested, extract and sort + # the listed versions and determine the highest + if [ "$GET_LAST_VERSION" != "no" ]; then + LASTVERSION=$( \ + for match in $(grep -E -o "$PATTERN" $ALL_LOGS); do + ver=${match##*ver=} + echo ${ver%%&*} + done | perl -e ' + use Devscripts::Versort; + while (<>) { push @versions, [$_]; } + @versions = Devscripts::Versort::versort(@versions); + print $versions[0][0]; ' | sed -e "s/\+/\\\+/g" + ) + + NEWPATTERN="fetch\.(cgi|php)\?pkg=$ESCAPED_PACKAGE&\ +arch=$ARCH&ver=$LASTVERSION&stamp=[[:digit:]]+" + else + NEWPATTERN=$PATTERN + fi + + for match in $(grep -E -o "$NEWPATTERN" $ALL_LOGS); do + ver=${match##*ver=} + ver=${ver%%&*} + arch=${match##*arch=} + arch=${arch%%&*} + match=$(echo $match | sed -e 's/\+/%2B/g') + # Mimic wget's behaviour, using a numerical suffix if needed, + # to support downloading several logs for a given tuple + # (unfortunately, -nc and -O means only the first file gets + # downloaded): + filename="${PACKAGE}_${ver}_${arch}.log" + if [ -f "$filename" ]; then + suffix=1 + while [ -f "$filename.$suffix" ]; do suffix=$((suffix+1)); done + filename="$filename.$suffix" + fi + wget -O "$filename" "$BASE/status/$match&raw=1" + done + + rm -f $ALL_LOGS + + if [ "$GET_LAST_VERSION" = "yes" ]; then + PATTERN=$NEWPATTERN + GET_LAST_VERSION=no + fi +} + +getbuildlog https://buildd.debian.org diff --git a/scripts/git-deborig.pl b/scripts/git-deborig.pl new file mode 100755 index 0000000..7ef342f --- /dev/null +++ b/scripts/git-deborig.pl @@ -0,0 +1,284 @@ +#!/usr/bin/perl + +# git-deborig -- try to produce Debian orig.tar using git-archive(1) + +# Copyright (C) 2016-2019 Sean Whitton <spwhitton@spwhitton.name> +# +# 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 <http://www.gnu.org/licenses/>. + +=head1 NAME + +git-deborig - try to produce Debian orig.tar using git-archive(1) + +=head1 SYNOPSIS + +B<git deborig> [B<--force>|B<-f>] [B<--just-print>|B<--just-print-tag-names>] [B<--version=>I<VERSION>] [I<COMMITTISH>] + +=head1 DESCRIPTION + +B<git-deborig> tries to produce the orig.tar you need for your upload +by calling git-archive(1) on an existing git tag or branch head. It +was written with the dgit-maint-merge(7) workflow in mind, but can be +used with other workflows. + +B<git-deborig> will try several common tag names. If this fails, or +if more than one of those common tags are present, you can specify the +tag or branch head to archive on the command line (I<COMMITTISH> above). + +B<git-deborig> will override gitattributes(5) that would cause the +contents of the tarball generated by git-archive(1) not to be +identical with the commitish archived: the B<export-subst> and +B<export-ignore> attributes. + +B<git-deborig> should be invoked from the root of the git repository, +which should contain I<debian/changelog>. + +=head1 OPTIONS + +=over 4 + +=item B<-f>|B<--force> + +Overwrite any existing orig.tar in the parent directory. + +=item B<--just-print> + +Instead of actually invoking git-archive(1), output information about +how it would be invoked. Ignores I<--force>. + +Note that running the git-archive(1) invocation outputted with this +option may not produce the same output. This is because +B<git-deborig> takes care to disables git attributes otherwise heeded +by git-archive(1), as detailed above. + +=item B<--just-print-tag-names> + +Instead of actually invoking git-archive(1), or even checking which +tags exist, print the tag names we would consider for the upstream +version number in the first entry in the Debian changelog, or that +supplied with B<--version>. + +=item B<--version=>I<VERSION> + +Instead of reading the new upstream version from the first entry in +the Debian changelog, use I<VERSION>. + +=back + +=head1 SEE ALSO + +git-archive(1), dgit-maint-merge(7), dgit-maint-debrebase(7) + +=head1 AUTHOR + +B<git-deborig> was written by Sean Whitton <spwhitton@spwhitton.name>. + +=cut + +use strict; +use warnings; + +use Getopt::Long; +use Git::Wrapper; +use Dpkg::Changelog::Parse; +use Dpkg::IPC; +use Dpkg::Version; +use List::Compare; +use String::ShellQuote; +use Try::Tiny; + +my $git = Git::Wrapper->new("."); + +# Sanity check #1 +try { + $git->rev_parse({ git_dir => 1 }); +} catch { + die "pwd doesn't look like a git repository ..\n"; +}; + +# Sanity check #2 +die "pwd doesn't look like a Debian source package ..\n" + unless (-e "debian/changelog"); + +# Process command line args +my $orig_args = join(" ", map { shell_quote($_) } ("git", "deborig", @ARGV)); +my $overwrite = ''; +my $user_version = ''; +my $user_ref = ''; +my $just_print = ''; +my $just_print_tag_names = ''; +GetOptions( + 'force|f' => \$overwrite, + 'just-print' => \$just_print, + 'just-print-tag-names' => \$just_print_tag_names, + 'version=s' => \$user_version +) || usage(); + +if (scalar @ARGV == 1) { + $user_ref = shift @ARGV; +} elsif (scalar @ARGV >= 2 + || ($just_print && $just_print_tag_names)) { + usage(); +} + +# Extract source package name from d/changelog and either extract +# version too, or parse user-supplied version +my $version; +my $changelog = Dpkg::Changelog::Parse->changelog_parse({}); +if ($user_version) { + $version = Dpkg::Version->new($user_version); +} else { + $version = $changelog->{Version}; +} + +# Sanity check #3 +die "version number $version is not valid ..\n" unless $version->is_valid(); + +my $source = $changelog->{Source}; +my $upstream_version = $version->version(); + +# Sanity check #4 +# Only complain if the user didn't supply a version, because the user +# is not required to include a Debian revision when they pass +# --version +die "this looks like a native package .." + if (!$user_version && $version->is_native()); + +# Convert the upstream version according to DEP-14 rules +my $git_upstream_version = $upstream_version; +$git_upstream_version =~ y/:~/%_/; +$git_upstream_version =~ s/\.(?=\.|$|lock$)/.#/g; + +# This list could be expanded if new conventions come into use +my @candidate_tags = ( + "$git_upstream_version", "v$git_upstream_version", + "upstream/$git_upstream_version" +); + +# Handle the --just-print-tag-names option +if ($just_print_tag_names) { + for my $candidate_tag (@candidate_tags) { + print "$candidate_tag\n"; + } + exit 0; +} + +# Default to gzip +my $compressor = "gzip -cn"; +my $compression = "gz"; +# Now check if we can use xz +if (-e "debian/source/format") { + open(my $format_fh, '<', "debian/source/format") + or die "couldn't open debian/source/format for reading"; + my $format = <$format_fh>; + chomp($format) if defined $format; + if ($format eq "3.0 (quilt)") { + $compressor = "xz -c"; + $compression = "xz"; + } + close $format_fh; +} + +my $orig = "../${source}_$upstream_version.orig.tar.$compression"; +die "$orig already exists: not overwriting without --force\n" + if (-e $orig && !$overwrite && !$just_print); + +if ($user_ref) { # User told us the tag/branch to archive + # We leave it to git-archive(1) to determine whether or not this + # ref exists; this keeps us forward-compatible + archive_ref_or_just_print($user_ref); +} else { # User didn't specify a tag/branch to archive + # Get available git tags + my @all_tags = $git->tag(); + + # See which candidate version tags are present in the repo + my $lc = List::Compare->new(\@all_tags, \@candidate_tags); + my @version_tags = $lc->get_intersection(); + + # If there is only one candidate version tag, we're good to go. + # Otherwise, let the user know they can tell us which one to use + if (scalar @version_tags > 1) { + print STDERR "tags ", join(", ", @version_tags), + " all exist in this repository\n"; + print STDERR +"tell me which one you want to make an orig.tar from: $orig_args TAG\n"; + exit 1; + } elsif (scalar @version_tags < 1) { + print STDERR "couldn't find any of the following tags: ", + join(", ", @candidate_tags), "\n"; + print STDERR +"tell me a tag or branch head to make an orig.tar from: $orig_args COMMITTISH\n"; + exit 1; + } else { + my $tag = shift @version_tags; + archive_ref_or_just_print($tag); + } +} + +sub archive_ref_or_just_print { + my $ref = shift; + + my $cmd = [ + 'git', '-c', "tar.tar.${compression}.command=${compressor}", + 'archive', "--prefix=${source}-${upstream_version}/", + '-o', $orig, $ref + ]; + if ($just_print) { + print "$ref\n"; + print "$orig\n"; + my @cmd_mapped = map { shell_quote($_) } @$cmd; + print "@cmd_mapped\n"; + } else { + my ($info_dir) = $git->rev_parse(qw|--git-path info/|); + my ($info_attributes) + = $git->rev_parse(qw|--git-path info/attributes|); + my ($deborig_attributes) + = $git->rev_parse(qw|--git-path info/attributes-deborig|); + + # sometimes the info/ dir may not exist + mkdir $info_dir unless (-e $info_dir); + + # For compatibility with dgit, we have to override any + # export-subst and export-ignore git attributes that might be set + rename $info_attributes, $deborig_attributes + if (-e $info_attributes); + my $attributes_fh; + unless (open($attributes_fh, '>', $info_attributes)) { + rename $deborig_attributes, $info_attributes + if (-e $deborig_attributes); + die "could not open $info_attributes for writing"; + } + print $attributes_fh "* -export-subst\n"; + print $attributes_fh "* -export-ignore\n"; + close $attributes_fh; + + spawn( + exec => $cmd, + wait_child => 1, + nocheck => 1 + ); + + # Restore situation before we messed around with git attributes + if (-e $deborig_attributes) { + rename $deborig_attributes, $info_attributes; + } else { + unlink $info_attributes; + } + } +} + +sub usage { + die +"usage: git deborig [--force|-f] [--just-print|--just-print-tag-names] [--version=VERSION] [COMMITTISH]\n"; +} diff --git a/scripts/grep-excuses.1 b/scripts/grep-excuses.1 new file mode 100644 index 0000000..111e33d --- /dev/null +++ b/scripts/grep-excuses.1 @@ -0,0 +1,65 @@ +.TH GREP-EXCUSES 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +grep-excuses \- search the testing excuses files for a specific maintainer +.SH SYNOPSIS +\fBgrep-excuses\fR [\fIoptions\fR] [\fImaintainer\fR|\fIpackage\fR] +.SH DESCRIPTION +\fBgrep-excuses\fR downloads the autoremovals and update_excuses.html files +and greps them +for the specified maintainer or package name. The \fBwget\fR package is +required for this script. If no name is given on the command line, +first the environment variable \fBDEBFULLNAME\fR is used if it is +defined, and failing that, the configuration variable described below +is used. +.SH OPTIONS +.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\-\-wipnity\fR, \fB\-w\fR +Get information from <https://qa.debian.org/excuses.php>. +One or more package names must be given when using this option. +.TP +.B \-\-help +Show a brief usage message. +.TP +.B \-\-version +Show version and copyright information. +.TP +.B \-\-autopkgtests +Investigate and show autopkgtest (ci.debian.net) failures +in your packages +but apparently caused by new versions of other packages +trying to migrate. +.RB ( \-\-no-autopkgtests +can be used to override GREP_EXCUSES_AUTOPKGTESTS.) +.TP +.B \-\-no\-autoremovals +Investigate and show only testing propagation excuses, not autoremovals. +.TP +\fB\-\-experimental\fR, \fB\-e\fR +Print pseudo-excuses for manual migration from experimental to unstable. +.TP +.B \-\-debug +Print debugging output to stderr (including url(s) fetched). +.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 GREP_EXCUSES_MAINTAINER +The default maintainer, email or package to grep for if none is +specified on the command line. +.TP +.B GREP_EXCUSES_AUTOPKGTESTS +Boolean: whether to show autopkgtest failures in other packages. +See +.BR \-\-autopkgtests . +.SH "SEE ALSO" +.BR devscripts.conf (5) +.SH AUTHOR +Joey Hess <joeyh@debian.org>; +modifications by Julian Gilbey <jdg@debian.org>. diff --git a/scripts/grep-excuses.pl b/scripts/grep-excuses.pl new file mode 100755 index 0000000..d42044e --- /dev/null +++ b/scripts/grep-excuses.pl @@ -0,0 +1,422 @@ +#!/usr/bin/perl +# vim: set ai shiftwidth=4 tabstop=4 expandtab: +# Grep debian testing excuses file. +# +# Copyright 2002 Joey Hess <joeyh@debian.org> +# Small mods Copyright 2002 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.006; +use strict; +use warnings; +use Data::Dumper; +use File::Basename; +use File::HomeDir; + +sub require_friendly ($) { + my ($mod) = @_; + return if eval "require $mod;"; + my $pkg = lc $mod; + $pkg =~ s/::/-/g; + $pkg = "lib$pkg-perl"; + die <<END; +$@ +grep-excuses: We need $mod. Try installing $pkg. +END +} + +# Needed for --wipnity option + +open DEBUG, ">/dev/null" or die $!; +my $do_autoremovals = 1; +my $do_autopkgtests; + +my $term_size_broken; + +sub have_term_size { + return ($term_size_broken ? 0 : 1) if defined $term_size_broken; + pop @INC if $INC[-1] eq '.'; + # Load the Term::Size module safely + eval { require Term::Size; }; + if ($@) { + if ($@ =~ /^Can\'t locate Term\/Size\.pm/) { + $term_size_broken + = "the libterm-size-perl package is not installed"; + } else { + $term_size_broken = "couldn't load Term::Size: $@"; + } + } else { + $term_size_broken = 0; + } + + return ($term_size_broken ? 0 : 1); +} + +my $progname = basename($0); +my $modified_conf_msg; + +my $url = 'https://release.debian.org/britney/excuses.yaml'; +my $url_experimental + = 'https://release.debian.org/britney/pseudo-excuses-experimental.yaml'; + +my $rmurl = 'https://udd.debian.org/cgi-bin/autoremovals.cgi'; +my $rmurl_yaml = 'https://udd.debian.org/cgi-bin/autoremovals.yaml.cgi'; + +my $wipnityurl = 'https://qa.debian.org/excuses.php?package='; +my $wipnityurl_experimental + = 'https://qa.debian.org/excuses.php?experimental=1&package='; + +# No longer use these - see bug#309802 +my $cachedir = File::HomeDir->my_home . "/.devscripts_cache/"; +my $cachefile = $cachedir . basename($url); +unlink $cachefile if -f $cachefile; + +sub usage { + print <<"EOF"; +Usage: $progname [options] [<maintainer>|<package>] + Grep the Debian update_excuses file to find out about the packages + of <maintainer> or <package>. If neither are given, use the configuration + file setting or the environment variable DEBFULLNAME to determine the + maintainer name. +Options: + --no-conf, --noconf Don\'t read devscripts config files; + must be the first option given + --wipnity, -w Check <https://qa.debian.org/excuses.php>. A package + name must be given when using this option. + --autopkgtests Investigate and show autopkgtest (ci.debian.net) failures + --no-autoremovals Do not investigate and report autoremovals + --experimental, -e Print pseudo-excuses for experimental + --help Show this help + --version Give version information + --debug Print debugging output to stderr + +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 2002 by Joey Hess <joeyh\@debian.org>, +and modifications are copyright 2002 by 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 wipnity { + die "$progname: Couldn't run wipnity: $term_size_broken\n" + unless have_term_size(); + + my $columns = Term::Size::chars(); + + if (system("command -v w3m >/dev/null 2>&1") != 0) { + die + "$progname: wipnity mode requires the w3m package to be installed\n"; + } + + while (my $package = shift) { + my $dump = `w3m -dump -cols $columns "$wipnityurl$package"`; + $dump =~ s/.*(Excuse for .*)\s+Maintainer page.*/$1/ms; + $dump =~ s/.*(No excuse for .*)\s+Maintainer page.*/$1/ms; + print($dump); + } +} + +# Now start by reading configuration files and then command line +# The next stuff is boilerplate + +my $string; + +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 = ( + 'GREP_EXCUSES_MAINTAINER' => '', + 'GREP_EXCUSES_AUTOPKGTESTS' => 0, + ); + 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; + + $string = $config_vars{'GREP_EXCUSES_MAINTAINER'}; + $do_autopkgtests = $config_vars{'GREP_EXCUSES_AUTOPKGTESTS'}; +} + +while (@ARGV and $ARGV[0] =~ /^-/) { + if ($ARGV[0] eq '--wipnity' or $ARGV[0] eq '-w') { + shift; + @ARGV = grep { $_ ne '' } @ARGV; + unless (@ARGV) { + die +"$progname: no package specified!\nTry $progname --help for help.\n"; + } + while (my ($i, $string) = each(@ARGV)) { + wipnity($string); + print "\n\n" unless $i == $#ARGV; + } + exit 0; + } + if ($ARGV[0] eq '--debug') { + open DEBUG, ">&STDERR" or die $!; + shift; + next; + } + if ($ARGV[0] eq '--experimental' or $ARGV[0] eq '-e') { + $do_autoremovals = 0; + $url = $url_experimental; + $wipnityurl = $wipnityurl_experimental; + shift; + next; + } + if ($ARGV[0] eq '--no-autoremovals') { $do_autoremovals = 0; shift; next; } + if ($ARGV[0] eq '--autopkgtests') { $do_autopkgtests = 1; shift; next; } + if ($ARGV[0] eq '--no-autopkgtests') { $do_autopkgtests = 0; shift; next; } + if ($ARGV[0] eq '--help') { usage(); exit 0; } + if ($ARGV[0] eq '--version') { print $version; exit 0; } + if ($ARGV[0] =~ /^--no-?conf$/) { + die +"$progname: $ARGV[0] is only acceptable as the first command-line option!\n"; + } + die +"$progname: unrecognised option $ARGV[0]; try $progname --help for help\n"; +} + +if (!$string and exists $ENV{'DEBFULLNAME'}) { + $string = $ENV{'DEBFULLNAME'}; +} + +if (@ARGV) { + $string = shift; +} +if ($string eq '') { + die +"$progname: no maintainer or package specified!\nTry $progname --help for help.\n"; +} +if (@ARGV) { + die "$progname: too many arguments! Try $progname --help for help.\n"; +} + +if (system("command -v wget >/dev/null 2>&1") != 0) { + die "$progname: this program requires the wget package to be installed\n"; +} + +sub grep_autoremovals () { + print DEBUG "Fetching $rmurl\n"; + + unless (open REMOVALS, "wget -q -O - $rmurl |") { + warn "$progname: wget $rmurl failed: $!\n"; + return; + } + + my $wantmaint = 0; + my %reportpkgs; + + while (<REMOVALS>) { + if (m%^https?:%) { + next; + } + if (m%^\S%) { + $wantmaint = m%^\Q$string\E\b%; + next; + } + if (m%^$%) { + $wantmaint = undef; + next; + } + if (defined $wantmaint && m%^\s+([0-9a-z][-.+0-9a-z]*):\s*(.*)%) { + next unless $wantmaint || $1 eq $string; + warn "$progname: package $1 repeated in $rmurl at line $.:\n$_" + if defined $reportpkgs{$1}; + $reportpkgs{$1} = $2; + next; + } + warn "$progname: unprocessed line $. in $rmurl:\n$_"; + } + $? = 0; + unless (close REMOVALS) { + my $rc = $? >> 8; + warn "$progname: fetch $rmurl failed ($rc $!)\n"; + } + + return unless %reportpkgs; + + print DEBUG "Fetching $rmurl_yaml\n"; + + unless (open REMOVALS, "wget -q -O - $rmurl_yaml |") { + warn "$progname: wget $rmurl_yaml failed: $!\n"; + return; + } + + my $reporting = 0; + while (<REMOVALS>) { + if (m%^([0-9a-z][-.+0-9a-z]*):$%) { + my $pkg = $1; + my $human = $reportpkgs{$pkg}; + delete $reportpkgs{$pkg}; + $reporting = !!defined $human; + if ($reporting) { + print "$pkg (AUTOREMOVAL)\n $human\n" or die $!; + } + next; + } + if (m%^[ \t]%) { + if ($reporting) { + print " ", $_ or die $!; + } + next; + } + if (m%^$% || m%^\#% || m{^---$}) { + next; + } + warn "$progname: unprocessed line $. in $rmurl_yaml:\n$_"; + } + + $? = 0; + unless (close REMOVALS) { + my $rc = $? >> 8; + warn "$progname: fetch $rmurl_yaml failed ($rc $!)\n"; + } + + foreach my $pkg (keys %reportpkgs) { + print "$pkg (AUTOREMOVAL)\n $reportpkgs{$pkg}\n" or die $!; + } +} + +grep_autoremovals() if $do_autoremovals; + +require_friendly qw(YAML::Syck); +{ + no warnings 'once'; + $YAML::Syck::LoadBlessed = 0; +} + +print DEBUG "Fetching $url\n"; + +my $yaml = `wget -q -O - '$url'`; +if ($? == -1) { + die "$progname: unable to run wget: $!\n"; +} elsif ($? >> 8) { + die "$progname: wget exited $?\n"; +} + +sub migration_headline ($) { + my ($source) = @_; + sprintf("%s (%s to %s)", + $source->{'item-name'}, + $source->{'old-version'}, + $source->{'new-version'}); +} + +sub print_migration_excuse_info ($;$) { + my ($source, $summary) = @_; + if (exists $source->{maintainer}) { + printf(" Maintainer: $source->{maintainer}\n"); + } + if (exists $source->{dependencies}) { + for my $blocker (@{ $source->{dependencies}{'blocked-by'} }) { + printf(" Depends: %s %s (not considered)\n", + $source->{'item-name'}, $blocker); + } + for my $after (@{ $source->{dependencies}{'migrate-after'} }) { + printf(" Depends: %s %s\n", $source->{'item-name'}, $after); + } + } + for my $excuse (@{ $source->{excuses} }) { + next if $summary and $excuse =~ m/^autopkgtest /; + $excuse =~ s@</?[^>]+>@@g; + $excuse =~ s@<@<@g; + $excuse =~ s@>@>@g; + print " $excuse\n"; + } +} + +my $excuses = YAML::Syck::Load($yaml); +for my $source (@{ $excuses->{sources} }) { + if ( + $source->{'item-name'} eq $string + || (exists $source->{maintainer} + && $source->{maintainer} =~ m/\b\Q$string\E\b/) + ) { + print migration_headline($source), "\n"; + print_migration_excuse_info($source); + } +} + +if ($do_autopkgtests) { + flush STDOUT or die $!; + require_friendly qw(DBI); + require_friendly qw(DBD::Pg); + my $dbh = DBI->connect('DBI:Pg:dbname=udd;host=udd-mirror.debian.net', + 'udd-mirror', 'udd-mirror', { RaiseError => 1 }); + # https://www.postgresql.org/docs/9.5/static/functions-matching.html + my $regexp = $string; + $regexp =~ s{[^0-9a-z]}{\\$&}ig; + $regexp = "\\y$regexp\\y"; + my $pkgs = $dbh->selectall_arrayref( + 'select distinct source from sources where' + . ' maintainer_name ~ ? or' + . ' maintainer_email ~ ?', + {}, $regexp, $regexp + ); + my %wantpkgs; + $wantpkgs{ $_->[0] }++ foreach @$pkgs; + + for my $source (@{ $excuses->{sources} }) { + my $autopkgtests = $source->{'policy_info'}{'autopkgtest'}; + foreach my $k (sort keys %$autopkgtests) { + $k =~ m{/} or next; + my ($testpkg, $testvsn) = ($`, $'); + $wantpkgs{$testpkg} or next; + my $arches = $autopkgtests->{$k}; + foreach my $arch (sort keys %$arches) { + my $info = $arches->{$arch}; + next + if grep { $_ eq $info->[0] } + qw(PASS NEUTRAL RUNNING OLD_PASS); + printf "\nautopkgtest regression\n"; + printf " in %s (%s) on %s\n", $testpkg, $testvsn, $arch; + printf " due to %s\n", migration_headline($source); + print "test info\n"; + print " $_\n" foreach @$info; + print "migration excuses for $source->{'item-name'}\n"; + print_migration_excuse_info($source, 1); + } + } + } +} + +exit 0; diff --git a/scripts/hardening-check.pl b/scripts/hardening-check.pl new file mode 100755 index 0000000..ad7f4e4 --- /dev/null +++ b/scripts/hardening-check.pl @@ -0,0 +1,684 @@ +#!/usr/bin/perl +# Report the hardening characteristics of a set of binaries. +# Copyright (C) 2009-2013 Kees Cook <kees@debian.org> +# License: GPLv2 or newer +use strict; +use warnings; +use Getopt::Long qw(:config no_ignore_case bundling); +use Pod::Usage; +use IPC::Open3; +use Symbol qw(gensym); +use Term::ANSIColor; +use IO::Select; + +my $skip_pie = 0; +my $skip_stackprotector = 0; +my $skip_fortify = 0; +my $skip_relro = 0; +my $skip_bindnow = 0; +my $skip_cfprotection = 0; +my $report_functions = 0; +my $find_libc_functions = 0; +my $color = 0; +my $lintian = 0; +my $verbose = 0; +my $debug = 0; +my $quiet = 0; +my $help = 0; +my $man = 0; + +GetOptions( + "nopie|p+" => \$skip_pie, + "nostackprotector|s+" => \$skip_stackprotector, + "nofortify|f+" => \$skip_fortify, + "norelro|r+" => \$skip_relro, + "nobindnow|b+" => \$skip_bindnow, + "nocfprotection|x+" => \$skip_cfprotection, + "report-functions|R!" => \$report_functions, + "find-libc-functions|F!" => \$find_libc_functions, + "color|c!" => \$color, + "lintian|l!" => \$lintian, + "verbose|v!" => \$verbose, + "debug!" => \$debug, + "quiet|q!" => \$quiet, + "help|h|?" => \$help, + "man|H" => \$man, +) or pod2usage(2); +pod2usage(1) if $help; +pod2usage(-exitstatus => 0, -verbose => 2, -noperldoc => 1) if $man; + +my $overall = 0; +my $rc = 0; +my $report = ""; +my @tags; +my %libc = ( + 'asprintf' => 1, + 'confstr' => 1, + 'dprintf' => 1, + 'fdelt' => 1, + 'fgets' => 1, + 'fgets_unlocked' => 1, + 'fgetws' => 1, + 'fgetws_unlocked' => 1, + 'fprintf' => 1, + 'fread' => 1, + 'fread_unlocked' => 1, + 'fwprintf' => 1, + 'getcwd' => 1, + 'getdomainname' => 1, + 'getgroups' => 1, + 'gethostname' => 1, + 'getlogin_r' => 1, + 'gets' => 1, + 'getwd' => 1, + 'longjmp' => 1, + 'mbsnrtowcs' => 1, + 'mbsrtowcs' => 1, + 'mbstowcs' => 1, + 'memcpy' => 1, + 'memmove' => 1, + 'mempcpy' => 1, + 'memset' => 1, + 'obstack_printf' => 1, + 'obstack_vprintf' => 1, + 'poll' => 1, + 'ppoll' => 1, + 'pread64' => 1, + 'pread' => 1, + 'printf' => 1, + 'ptsname_r' => 1, + 'read' => 1, + 'readlink' => 1, + 'readlinkat' => 1, + 'realpath' => 1, + 'recv' => 1, + 'recvfrom' => 1, + 'snprintf' => 1, + 'sprintf' => 1, + 'stpcpy' => 1, + 'stpncpy' => 1, + 'strcat' => 1, + 'strcpy' => 1, + 'strncat' => 1, + 'strncpy' => 1, + 'swprintf' => 1, + 'syslog' => 1, + 'ttyname_r' => 1, + 'vasprintf' => 1, + 'vdprintf' => 1, + 'vfprintf' => 1, + 'vfwprintf' => 1, + 'vprintf' => 1, + 'vsnprintf' => 1, + 'vsprintf' => 1, + 'vswprintf' => 1, + 'vsyslog' => 1, + 'vwprintf' => 1, + 'wcpcpy' => 1, + 'wcpncpy' => 1, + 'wcrtomb' => 1, + 'wcscat' => 1, + 'wcscpy' => 1, + 'wcsncat' => 1, + 'wcsncpy' => 1, + 'wcsnrtombs' => 1, + 'wcsrtombs' => 1, + 'wcstombs' => 1, + 'wctomb' => 1, + 'wmemcpy' => 1, + 'wmemmove' => 1, + 'wmempcpy' => 1, + 'wmemset' => 1, + 'wprintf' => 1, +); + +# Report a good test. +sub good { + my ($name, $msg_color, $msg) = @_; + $msg_color = colored($msg_color, 'green') if $color; + if (defined $msg) { + $msg_color .= $msg; + } + good_msg("$name: $msg_color"); +} + +sub good_msg($) { + my ($msg) = @_; + if ($quiet == 0) { + $report .= "\n$msg"; + } +} + +sub unknown { + my ($name, $msg) = @_; + $msg = colored($msg, 'yellow') if $color; + good_msg("$name: $msg"); +} + +# Report a failed test, possibly ignoring it. +sub bad($$$$$) { + my ($name, $file, $long_name, $msg, $ignore) = @_; + + $msg = colored($msg, 'red') if $color; + + $msg = "$long_name: " . $msg; + if ($ignore) { + $msg .= " (ignored)"; + } else { + $rc = 1; + if ($lintian) { + push(@tags, "$name:$file"); + } + } + $report .= "\n$msg"; +} + +# Safely run list-based command line and return stdout. +sub output(@) { + my (@cmd) = @_; + my ($pid, $stdout, $stderr); + if ($debug) { + print join(" ", @cmd), "\n"; + } + $stdout = gensym; + $stderr = gensym; + $pid = open3(gensym, $stdout, $stderr, @cmd); + + my $selector = IO::Select->new(); + $selector->add($stdout); + $selector->add($stderr); + + my $collect_out = ""; + my $collect_err = ""; + + while (my @ready = $selector->can_read()) { + foreach my $fh (@ready) { + my $buf; + my $len = sysread($fh, $buf, 4096); + if ($len == 0) { + $selector->remove($fh); + next; + } + + if ($fh == $stdout) { + $collect_out .= $buf; + } else { + $collect_err .= $buf; + } + } + } + + waitpid($pid, 0); + my $rc = $?; + if ($rc != 0) { + print STDERR $collect_err; + return ""; + } + return $collect_out; +} + +# Find the libc used in this executable, if any. +sub find_libc($) { + my ($file) = @_; + my $ldd = output("ldd", $file); + $ldd =~ /^\s*libc\.so\.\S+\s+\S+\s+(\S+)/m; + return $1 || ""; +} + +sub find_functions($$) { + my ($file, $undefined) = @_; + my (%funcs); + + # Catch "NOTYPE" for object archives. + my $func_regex = " (I?FUNC|NOTYPE) "; + + my $relocs = output("readelf", "-sW", $file); + for my $line (split("\n", $relocs)) { + next if ($line !~ /$func_regex/); + next if ($undefined && $line !~ /$func_regex.* UND /); + + $line =~ s/ \([0-9]+\)$//; + $line =~ s/.* //; + $line =~ s/@.*//; + $funcs{$line} = 1; + } + + return \%funcs; +} + +$ENV{'LANG'} = "C"; + +if ($find_libc_functions) { + pod2usage(1) if (!defined($ARGV[0])); + my $libc_path = find_libc($ARGV[0]); + + my $funcs = find_functions($libc_path, 0); + for my $func (sort(keys(%{$funcs}))) { + if ($func =~ /^__(\S+)_chk$/) { + print " '$1' => 1,\n"; + } + } + exit(0); +} +die "List of libc functions not defined!" if (scalar(keys %libc) < 1); + +my $name; +foreach my $file (@ARGV) { + $rc = 0; + my $elf = 1; + + $report = "$file:"; + @tags = (); + + # Get program headers. + my $PROG_REPORT = output("readelf", "-lW", $file); + if (length($PROG_REPORT) == 0) { + $overall = 1; + next; + } + + # Get ELF headers. + my $DYN_REPORT = output("readelf", "-dW", $file); + + # Get disassembly + my $DISASM + = output("objdump", "-d", "--no-show-raw-insn", "-M", "intel", $file); + + # Get notes + my $NOTES = output("readelf", "-n", $file); + + # Get list of all symbols needing external resolution. + my $functions = find_functions($file, 1); + + # PIE + # First, verify this is an executable, not a library. This seems to be + # best seen by checking for the PHDR program header. + $name = " Position Independent Executable"; + $PROG_REPORT =~ /^Elf file type is (\S+)/m; + my $elftype = $1 || ""; + if ($elftype eq "DYN") { + if ($PROG_REPORT =~ /^ *\bPHDR\b/m) { + + # Executable, DYN ELF type. + good($name, "yes"); + } else { + # Shared library, DYN ELF type. + good($name, "no, regular shared library (ignored)"); + } + } elsif ($elftype eq "EXEC") { + + # Executable, EXEC ELF type. + bad("no-pie", $file, $name, "no, normal executable!", $skip_pie); + } else { + $elf = 0; + + # Is this an ar file with objects? + open(AR, "<$file"); + my $header = <AR>; + close(AR); + if ($header eq "!<arch>\n") { + good($name, "no, object archive (ignored)"); + } else { + # ELF type is neither DYN nor EXEC. + bad("unknown-elf", $file, $name, + "not a known ELF type!? ($elftype)", 0); + } + } + + # Stack-protected + $name = " Stack protected"; + if (defined($functions->{'__stack_chk_fail'}) + || (!$elf && defined($functions->{'__stack_chk_fail_local'}))) { + good($name, "yes"); + } else { + if (%{$functions} eq 0) { + unknown($name, "unknown, no symbols found"); + } else { + bad("no-stackprotector", $file, $name, "no, not found!", + $skip_stackprotector); + } + } + + # Fortified Source + $name = " Fortify Source functions"; + my @unprotected; + my @protected; + for my $name (keys(%libc)) { + if (defined($functions->{$name})) { + push(@unprotected, $name); + } + if (defined($functions->{"__${name}_chk"})) { + push(@protected, $name); + } + } + if ($#protected > -1) { + if ($#unprotected == -1) { + + # Certain. + good($name, "yes"); + } else { + # Vague, due to possible compile-time optimization, + # multiple linkages, etc. Assume "yes" for now. + good($name, "yes", " (some protected functions found)"); + } + } else { + if ($#unprotected == -1) { + unknown($name, "unknown, no protectable libc functions used"); + } else { + # Vague, since it's possible to have the compile-time + # optimizations do away with them, or be unverifiable + # at runtime. Assume "no" for now. + bad("no-fortify-functions", $file, $name, + "no, only unprotected functions found!", + $skip_fortify); + } + } + if ($verbose) { + for my $name (@unprotected) { + good_msg("\tunprotected: $name"); + } + for my $name (@protected) { + good_msg("\tprotected: $name"); + } + } + + # Format + # Unfortunately, I haven't thought of a way to test for this after + # compilation. What it really needs is a lintian-like check that + # reviews the build logs and looks for the warnings, or that the + # argument is changed to use -Werror=format-security to stop the build. + + # RELRO + $name = " Read-only relocations"; + if ($PROG_REPORT =~ /^ *\bGNU_RELRO\b/m) { + good($name, "yes"); + } else { + if ($elf) { + bad("no-relro", $file, $name, "no, not found!", $skip_relro); + } else { + good($name, "no", ", non-ELF (ignored)"); + } + } + + # BIND_NOW + # This marking keeps changing: + # 0x0000000000000018 (BIND_NOW) + # 0x000000006ffffffb (FLAGS) Flags: BIND_NOW + # 0x000000006ffffffb (FLAGS_1) Flags: NOW + + $name = " Immediate binding"; + if ( $DYN_REPORT =~ /^\s*\S+\s+\(BIND_NOW\)/m + || $DYN_REPORT =~ /^\s*\S+\s+\(FLAGS\).*\bBIND_NOW\b/m + || $DYN_REPORT =~ /^\s*\S+\s+\(FLAGS_1\).*\bNOW\b/m) { + good($name, "yes"); + } else { + if ($elf) { + bad("no-bindnow", $file, $name, "no, not found!", $skip_bindnow); + } else { + good($name, "no", ", non-ELF (ignored)"); + } + } + + # For stack clash we need to look for a specific sequence of + # instructions in the objdump disassembly + $name = " Stack clash protection"; + my $index = 0; + my $cmp_addr = 0; + my @patterns = ( + qr/^\s+([0-9a-f]+):\s+cmp\s+(rsp.*|.*0x1000)/, + qr/^\s+[0-9a-f]+:\s+j[eb]\s+([x0-9a-f]+)/, + qr/^\s+[0-9a-f]+:\s+sub\s+(.*,0x1000)/, + qr/^\s+[0-9a-f]+:\s+or\s+(.*,0x0)/, + qr/^\s+([0-9a-f]+):\s+(jmp\s+([x0-9a-f]+)|cmp\s+rsp,.*)/, + qr/^\s+([0-9a-f]+):\s+jne\s+([x0-9a-f]+)/ + ); + my $found = 0; + foreach my $line (split /\n/, $DISASM) { + + # look for each regex from patterns in succession - they all + # should be consecutive in the binary so we always fall back to + # index 0 if we fail to find the next one + if (my @matches = ($line =~ $patterns[$index])) { + if ($index == 0) { + $cmp_addr = hex($matches[0]); + } elsif ($index == 4) { + + # this could be either the jmp or cmp - if is jump then + # this is the last instruction in the sequence otherwise + # cmp has a jne following for index 5 + if ($matches[1] =~ /^jmp.*/) { + my $arg = hex($matches[2]); + if ($arg == $cmp_addr) { + good($name, "yes"); + $found = 1; + last; + } else { + # since the expected instructions should be + # contiguous, always fall back to zero on failure + $index = 0; + next; + } + } + + # nothing to do for the cmp case + } elsif ($index == 5) { + my $arg = hex($matches[1]); + if ($arg == $cmp_addr + 5) { + good($name, "yes"); + $found = 1; + last; + } else { + # since the expected instructions should be + # contiguous, always fall back to zero on failure + $index = 0; + next; + } + } + ++$index; + } else { + $index = 0; + } + } + if (!$found) { + unknown($name, + "unknown, no -fstack-clash-protection instructions found"); + } + + # For cf-protection look for x86 feature: IBT, SHSTK + $name = " Control flow integrity"; + if ($NOTES =~ /^\s+Properties: x86 feature: IBT, SHSTK/m) { + good($name, "yes"); + } else { + bad("no-cfprotection", $file, $name, "no, not found!", + $skip_cfprotection); + } + + if (!$lintian && (!$quiet || $rc != 0)) { + print $report, "\n"; + } + + if ($report_functions) { + for my $name (keys(%{$functions})) { + print $name, "\n"; + } + } + + if (!$lintian && $rc) { + $overall = $rc; + } + + if ($lintian) { + for my $tag (@tags) { + print $tag, "\n"; + } + } +} + +exit($overall); + +__END__ + +=head1 NAME + +hardening-check - check binaries for security hardening features + +=head1 SYNOPSIS + +hardening-check [options] [ELF ...] + +Examine a given set of ELF binaries and check for several security hardening +features, failing if they are not all found. + +=head1 DESCRIPTION + +This utility checks a given list of ELF binaries for several security +hardening features that can be compiled into an executable. These +features are: + +=over 8 + +=item B<Position Independent Executable> + +This indicates that the executable was built in such a way (PIE) that +the "text" section of the program can be relocated in memory. To take +full advantage of this feature, the executing kernel must support text +Address Space Layout Randomization (ASLR). + +=item B<Stack Protected> + +This indicates that there is evidence that the ELF was compiled with the +L<gcc(1)> option B<-fstack-protector> (e.g. uses B<__stack_chk_fail>). The +program will be resistant to having its stack overflowed. + +When an executable was built without any character arrays being allocated +on the stack, this check will lead to false alarms (since there is no +use of B<__stack_chk_fail>), even though it was compiled with the correct +options. + +=item B<Fortify Source functions> + +This indicates that the executable was compiled with +B<-D_FORTIFY_SOURCE=2> and B<-O1> or higher. This causes certain unsafe +glibc functions with their safer counterparts (e.g. B<strncpy> instead +of B<strcpy>), or replaces calls that are verifiable at runtime with the +runtime-check version (e.g. B<__memcpy_chk> insteade of B<memcpy>). + +When an executable was built such that the fortified versions of the glibc +functions are not useful (e.g. use is verified as safe at compile time, or +use cannot be verified at runtime), this check will lead to false alarms. +In an effort to mitigate this, the check will pass if any fortified function +is found, and will fail if only unfortified functions are found. Uncheckable +conditions also pass (e.g. no functions that could be fortified are found, or +not linked against glibc). + +=item B<Read-only relocations> + +This indicates that the executable was build with B<-Wl,-z,relro> to +have ELF markings (RELRO) that ask the runtime linker to mark any +regions of the relocation table as "read-only" if they were resolved +before execution begins. This reduces the possible areas of memory in +a program that can be used by an attacker that performs a successful +memory corruption exploit. + +=item B<Immediate binding> + +This indicates that the executable was built with B<-Wl,-z,now> to have +ELF markings (BIND_NOW) that ask the runtime linker to resolve all +relocations before starting program execution. When combined with RELRO +above, this further reduces the regions of memory available to memory +corruption attacks. + +=back + +=head1 OPTIONS + +=over 8 + +=item B<--nopie>, B<-p> + +Do not require that the checked binaries be built as PIE. + +=item B<--nostackprotector>, B<-s> + +Do not require that the checked binaries be built with the stack protector. + +=item B<--nofortify>, B<-f> + +Do not require that the checked binaries be built with Fortify Source. + +=item B<--norelro>, B<-r> + +Do not require that the checked binaries be built with RELRO. + +=item B<--nobindnow>, B<-b> + +Do not require that the checked binaries be built with BIND_NOW. + +=item B<--nocfprotection>, B<-b> + +Do not require that the checked binaries be built with control flow protection. + +=item B<--quiet>, B<-q> + +Only report failures. + +=item B<--verbose>, B<-v> + +Report verbosely on failures. + +=item B<--report-functions>, B<-R> + +After the report, display all external functions needed by the ELF. + +=item B<--find-libc-functions>, B<-F> + +Instead of the regular report, locate the libc for the first ELF on the +command line and report all the known "fortified" functions exported by +libc. + +=item B<--color>, B<-c> + +Enable colorized status output. + +=item B<--lintian>, B<-l> + +Switch reporting to lintian-check-parsable output. + +=item B<--debug> + +Report some debugging during processing. + +=item B<--help>, B<-h>, B<-?> + +Print a brief help message and exit. + +=item B<--man>, B<-H> + +Print the manual page and exit. + +=back + +=head1 RETURN VALUE + +When all checked binaries have all checkable hardening features detected, +this program will finish with an exit code of 0. If any check fails, the +exit code with be 1. Individual checks can be disabled via command line +options. + +=head1 AUTHOR + +Kees Cook <kees@debian.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2009-2013 Kees Cook <kees@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; version 2 or later. + +=head1 SEE ALSO + +L<gcc(1)>, L<hardening-wrapper(1)> + +=cut diff --git a/scripts/list-unreleased.1 b/scripts/list-unreleased.1 new file mode 100644 index 0000000..824df4f --- /dev/null +++ b/scripts/list-unreleased.1 @@ -0,0 +1,23 @@ +.TH LIST-UNRELEASED 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +list-unreleased \- display UNRELEASED packages +.SH SYNOPSIS +\fBlist-unreleased\fR [\fIarg\fR ...] [\fIpath\fR ...] +.SH DESCRIPTION +Searches for packages whose changelogs indicate there are pending changes +(UNRELEASED) and either lists them or displays the relevant changelog entry. +.PP +By default it searches for packages under the current directory. If a path +is specified it will look for packages under that directory instead. +.SH OPTIONS +.TP +.B -c +Display pending changes. +.TP +.B -R +Don't recurse into subdirectories looking for packages. +.SH "SEE ALSO" +.BR debchange (1) +.SH AUTHOR +\fBlist-unreleased\fR was written by Frans Pop <elendil@planet.nl>. +This manual page was written by Joey Hess <joeyh@debian.org>. diff --git a/scripts/list-unreleased.bash_completion b/scripts/list-unreleased.bash_completion new file mode 100644 index 0000000..e560129 --- /dev/null +++ b/scripts/list-unreleased.bash_completion @@ -0,0 +1,13 @@ +# /usr/share/bash-completion/completions/list-unreleased +# Bash command completion for ‘list-unreleased(1)’. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +complete -W '-c -R' -o filenames -d list-unreleased + + +# Local variables: +# coding: utf-8 +# mode: shell-script +# indent-tabs-mode: nil +# End: +# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 : diff --git a/scripts/list-unreleased.sh b/scripts/list-unreleased.sh new file mode 100755 index 0000000..05ed06b --- /dev/null +++ b/scripts/list-unreleased.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# Script searches for packages with pending changes (UNRELEASED) and +# either lists them or displays the relevant changelog entry. + +# Usage: list-unreleased [-cR] +# -c : display pending changes +# -R : don't recurse + +# Copyright: Frans Pop <elendil@planet.nl>, 2007 +# 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. + +PATHS="" +DO_CL="" +RECURSE=1 + +while true; do + case "$1" in + "") + break ;; + -c) + DO_CL=1 + ;; + -R) + RECURSE= + ;; + -*) + echo "unrecognized argument '$1'" + exit 1 + ;; + *) + PATHS="${PATHS:+$PATHS }$1" + ;; + esac + shift +done + +[ "$PATHS" ] || PATHS=. + +vcs_dirs='\(\.\(svn\|hg\|git\|bzr\)\|_darcs\|_MTN\|CVS\)' +get_list() { + local path="$1" + + for dir in $( + if [ "$RECURSE" ]; then + find "$path" -type d ! -regex "$vcs_dirs" + else + find "$path" -maxdepth 1 -type d ! -regex "$vcs_dirs" + fi + ); do + changelog="$dir/debian/changelog" + if [ -f "$changelog" ] ; then + if head -n1 "$changelog" | grep -q UNRELEASED; then + echo $dir + fi + fi + done | sort +} + +print_cl() { + local package="$1" + changelog="$package/debian/changelog" + + # Check if more than one UNRELEASED entry at top of changelog + Ucount=$(grep "^[^ ]" $changelog | \ + head -n2 | grep -c UNRELEASED) + if [ $Ucount -eq 1 ]; then + sed -n "1,/^ --/p" $changelog + else + echo "ERROR: changelog has more than one UNRELEASED entry!" + # Second sed is to add back a blank line between entries + sed -n "/^[^ ].*UNRELEASED/,/^ --/p" $changelog | \ + sed '2,$s/^\([^ ]\)/\n\1/' + fi +} + +first="" +for path in $PATHS; do + if [ -z "$DO_CL" ]; then + echo "$(get_list "$path" | sed "s:^\./::")" + else + for package in $(get_list "$path"); do + [ -z "$first" ] || echo -e "\n====================\n" + first=1 + + print_cl "$package" + done + fi +done diff --git a/scripts/ltnu.pod b/scripts/ltnu.pod new file mode 100644 index 0000000..b7d78fe --- /dev/null +++ b/scripts/ltnu.pod @@ -0,0 +1,108 @@ +=head1 NAME + +ltnu - lists packages of a maintainer ordered by last upload + +=head1 SYNOPSIS + +B<env> DEBEMAIL=I<maintainer> B<ltnu> [-m] + +B<ltnu> [-m] I<maintainer> + +B<ltnu> --help + +=head1 DESCRIPTION + +B<ltnu> (Long Time No Upload) queries the public mirror of the +Ultimate Debian Database (udd-mirror.debian.net) for all uploads of +packages by the given uploader or maintainer and displays them ordered +by the last upload of that package to Debian Unstable, oldest uploads +first. + +Its primary purpose is to check which of your own or your team's +packages haven't been uploaded for a long time and likely need a +packaging revamp. It's less suitable for MIA team purposes as it +doesn't make a difference with regards to who actually uploaded a +package. + +=head1 OPTIONS + +=over 4 + +=item -m + +Only search in the Maintainer field and ignore the Uploaders field. + +=back + +=head1 PARAMETERS + +The maintainer/uploader to query can be given either by setting +C<$DEBEMAIL> as environment variable or as single commandline parameter. + +If a commandline parameter does not contain an C<@>, C<@debian.org> is +appended, e.g. C<ltnu abe> queries for C<abe@debian.org>. + +Exceptions are some shortcuts for common, long e-mail addresses. So +far implemented shortcuts: + +=over 13 + +=item pkg-gnustep + +pkg-gnustep-maintainers@lists.alioth.debian.org + +=item pkg-perl + +pkg-perl-maintainers@lists.alioth.debian.org + +=item pkg-zsh + +pkg-zsh-devel@lists.alioth.debian.org + +=item qa + +packages@qa.debian.org + +=back + +=head1 ENVIRONMENT + +The following environment variables are honoured: + +=over + +=item DEBEMAIL + +Used for querying if no parameter is given. + +=item PAGER + +Used by B<psql> as pager. + +=back + +=head1 EXAMPLE + + $ ltnu pkg-zsh + source | ver | uploaded + -------------------------+--------------+------------------------ + zgen | 0~20150919-3 | 2016-08-24 04:55:31+00 + zplug | 2.4.1-1 | 2017-01-13 09:51:26+00 + zsh-syntax-highlighting | 0.6.0-1 | 2017-08-30 09:06:26+00 + zsh | 5.4.2-2 | 2017-11-02 20:56:55+00 + (4 rows) + +=head1 DEPENDENCIES + +B<ltnu> uses the PostgreSQL client command B<psql> and hence needs +Debian's B<postgresql-client> package to be installed. + +=head1 AUTHOR, COPYRIGHT, LICENSE + +Copyright 2017 Axel Beckert <abe@debian.org>. Licensed under the GNU +General Public License, version 2 or later. + +=head1 SEE ALSO + +L<https://udd-mirror.debian.net/>, L<https://udd.debian.org/>, +L<https://wiki.debian.org/UltimateDebianDatabase> diff --git a/scripts/ltnu.sh b/scripts/ltnu.sh new file mode 100755 index 0000000..c0fbaae --- /dev/null +++ b/scripts/ltnu.sh @@ -0,0 +1,86 @@ +#!/bin/sh + +# Copyright 2017 Axel Beckert <abe@debian.org>. +# Licensed under the GNU GPL, version 2 or later. + +set -e + +if [ "${1}" = '-h' -o "${1}" = '--help' ]; then + echo "${0} (Long Time No Upload) queries the public mirror of the +Ultimate Debian Database (UDD) for all uploads of packages by the +given uploader or maintainer and displays them ordered by the last +upload of that package, oldest uploads first. + +The maintainer/uploader to query can be given either by setting +\$DEBEMAIL as environment variable or as single commandline parameter. + +If a commandline parameter does not contain an \"@\", \"@debian.org\" +is appended, e.g. \"${0} abe\" queries for \"abe@debian.org\". + +Exceptions are some shortcuts for common, long e-mail addresses. So +far implemented shortcuts: + +* pkg-perl = pkg-perl-maintainers@lists.alioth.debian.org +* pkg-zsh = pkg-zsh-devel@lists.alioth.debian.org +* pkg-gnustep = pkg-gnustep-maintainers@lists.alioth.debian.org +" + exit 0 +fi + +if [ ! -x /usr/bin/psql ]; then + echo "/usr/bin/psql not found or not executable" 1>&2 + echo "${0} requires a PostgreSQL client (psql) to be installed." 1>&2 + exit 2 +fi + +# Some option parsing +QUERY_UPLOADER=1 +if [ "$1" = "-m" ]; then + QUERY_UPLOADER=0 + shift +fi + +MAINT="${DEBEMAIL}" +if [ -n "${1}" ]; then + if echo "${1}" | grep -qF @; then + MAINT="${1}" + elif [ "${1}" = "pkg-gnustep" ]; then + MAINT="pkg-gnustep-maintainers@lists.alioth.debian.org" + elif [ "${1}" = "pkg-perl" ]; then + MAINT="pkg-perl-maintainers@lists.alioth.debian.org" + elif [ "${1}" = "pkg-zsh" ]; then + MAINT="pkg-zsh-devel@lists.alioth.debian.org" + elif [ "${1}" = "qa" ]; then + MAINT="packages@qa.debian.org" + else + MAINT="${1}@debian.org" + fi +fi + +if [ -z "${MAINT}" ]; then + echo "${0} requires either the environment variable \$DEBEMAIL to be set or a single parameter." 1>&2 + exit 1; +fi + +if [ -z "${PAGER}" -o "${PAGER}" = "less" ]; then + export PAGER="less -S" +fi + +UPLOADER='' +if [ "$QUERY_UPLOADER" = 1 ]; then + UPLOADER=" or uploaders like '%<${MAINT}>%'" +fi + +env PGPASSWORD=udd-mirror psql --host=udd-mirror.debian.net --user=udd-mirror udd --command=" +select source, + max(version) as ver, + max(date) as uploaded +from upload_history +where distribution='unstable' and + source in (select source + from sources + where ( maintainer_email='${MAINT}' $UPLOADER ) and + release='sid') +group by source +order by max(date) asc; +" diff --git a/scripts/manpage-alert.1 b/scripts/manpage-alert.1 new file mode 100644 index 0000000..833dcf6 --- /dev/null +++ b/scripts/manpage-alert.1 @@ -0,0 +1,35 @@ +.TH MANPAGE-ALERT 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +manpage-alert \- check for binaries without corresponding manpages +.SH SYNOPSIS +\fBmanpage-alert\fR [\fIoptions\fR] [\fIpaths\fR] +.SH DESCRIPTION +\fBmanpage-alert\fR searches the given list of paths for binaries without +corresponding manpages. +.P +If no \fIpaths\fR are specified on the command line, the path list +\fI/bin /sbin /usr/bin /usr/sbin /usr/games\fR will be assumed. +.SH OPTIONS +.TP +.BR \-h\fR, \fB\-\-help +Show a summary of options. +.TP +.BR \-V\fR, \fB\-\-version +Show version and copyright information. +.TP +.BR \-f\fR, \fB\-\-file +Show filenames of missing manpages without any leading text. +.TP +.BR \-p\fR, \fB\-\-package +Show filenames of missing manpages with their package name. +.TP +.BR \-n\fR, \fB\-\-no\-stat +Do not show statistics at the end. +.SH AUTHOR +\fBmanpage-alert\fR was written by Branden Robinson and modified by +Julian Gilbey <jdg@debian.org> and Adam D. Barratt +<debian\-bts@adam\-barratt.org.uk> (who also wrote this manpage) for the +devscripts package. +.P +This manpage and the associated program are licensed under the terms of +the GPL, version 2 or later. diff --git a/scripts/manpage-alert.sh b/scripts/manpage-alert.sh new file mode 100755 index 0000000..626b377 --- /dev/null +++ b/scripts/manpage-alert.sh @@ -0,0 +1,147 @@ +#!/bin/sh -e +# +# Copyright 2005 Branden Robinson +# Changes copyright 2007 by their respective authors +# +# 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. + +usage() { + cat <<EOF +Usage: manpage-alert [options] [paths] + Options: + -h, --help This usage screen. + -V, --version Display the version and copyright information. + -f, --file Show filenames of missing manpages + without any leading text. + -p, --package Show filenames of missing manpages + with their package name. + -n, --no-stat Do not show statistics at the end. + + This script will locate executables in the given paths with manpage + outputs for which no manpage is available and its statistics. + + If no paths are specified on the command line, "/bin /sbin /usr/bin + /usr/sbin /usr/games" will be used by default. +EOF +} + +version() { + cat <<EOF +This is manpage-alert, from the Debian devscripts package, version ###VERSION###. +This code is (C) 2005 by Branden Robinson, 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 +} + +showpackage() { + F1="$1" + P1="$(LANG=C dpkg-query -S "$F1" 2> /dev/null || true )" + Q1=""; R1=""; Q2=""; R2="" + if [ -n "$P1" ]; then + Q1="$(echo "$P1" | grep -v "^diversion by" || true)" + R1="$(echo "$P1" | sed -ne 's/^diversion by \(.*\) to:.*$/\1/p'): $F1" + fi + # symlink may be created by postinst script for alternatives etc., + if [ -z "$Q1" ] && [ -L "$F1" ]; then + F2=$(readlink -f "$F1") + P2="$(LANG=C dpkg-query -S "$F2" 2> /dev/null || true )" + if [ -n "$P2" ]; then + Q2="$(echo "$P2" | grep -v "^diversion by" || true)" + R2="$(echo "$P2" | sed -ne 's/^diversion by \(.*\) to:.*$/\1/p'): $F2" + fi + fi + if [ -n "$Q1" ]; then + echo "$Q1" + elif [ -n "$R1" ]; then + echo "$R1 (diversion)" + elif [ -n "$Q2" ]; then + echo "unknown_package: $F1 -> $Q2" + elif [ -n "$R2" ]; then + echo "unknown_package: $F1 -> $R2 (diversion)" + else + echo "unknown_package: $F1" + fi +} + +SHOWPACKAGE=DEFAULT +SHOWSTAT=TRUE + +while [ -n "$1" ]; do + case "$1" in + -h|--help) usage; exit 0;; + -V|--version) version; exit 0;; + -p|--package) SHOWPACKAGE=PACKAGE + shift + ;; + -f|--file) SHOWPACKAGE=FILE + shift + ;; + -n|--no-stat) SHOWSTAT=FALSE + shift + ;; + *) break + ;; + esac +done + +if [ $# -lt 1 ]; then + # check if we're running on a usrmerge system + if [ -L /bin -a -L /sbin ]; then + set -- /usr/bin /usr/sbin /usr/games + else + set -- /bin /sbin /usr/bin /usr/sbin /usr/games + fi +fi + +NUM_EXECUTABLES=0 +NUM_MANPAGES_FOUND=0 +NUM_MANPAGES_MISSING=0 + +for DIR in "$@"; do + for F in "$DIR"/*; do + # Skip as it's a symlink to /usr/bin + if [ "$F" = "/usr/bin/X11" ]; then continue; fi + NUM_EXECUTABLES=$(( NUM_EXECUTABLES + 1 )) + + if OUT=$(man -w -S 1:8:6 "${F##*/}" 2>&1 > /dev/null); then + NUM_MANPAGES_FOUND=$(( NUM_MANPAGES_FOUND + 1 )) + else + if [ $SHOWPACKAGE = "PACKAGE" ]; then + # echo "<packagename>: <filename>" + showpackage "$F" + elif [ $SHOWPACKAGE = "FILE" ]; then + # echo "<filename>" + echo "$F" + else + # echo "No manual entry for <filename>" + echo "$OUT" | perl -ne "next if /^.*'man 7 undocumented'.*$/;" \ + -e "s,(\W)\Q${F##*/}\E(?:\b|$),\1$F,; s,//,/,; print;" + fi + NUM_MANPAGES_MISSING=$(( NUM_MANPAGES_MISSING + 1 )) + fi + done +done + +if [ $SHOWSTAT = "TRUE" ]; then +echo +printf "Of %d commands, found manpages for %d (%d missing).\n" \ + $NUM_EXECUTABLES \ + $NUM_MANPAGES_FOUND \ + $NUM_MANPAGES_MISSING +fi + +# vim:set ai et sw=4 ts=4 tw=80: diff --git a/scripts/mass-bug.pl b/scripts/mass-bug.pl new file mode 100755 index 0000000..805d9ef --- /dev/null +++ b/scripts/mass-bug.pl @@ -0,0 +1,570 @@ +#!/usr/bin/perl + +# mass-bug: mass-file a bug report against a list of packages +# For options, see the usage message below. +# +# Copyright 2006 by Joey Hess <joeyh@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/>. + +=head1 NAME + +mass-bug - mass-file a bug report against a list of packages + +=head1 SYNOPSIS + +B<mass-bug> [I<options>] B<--subject=">I<bug subject>B<"> I<template package-list> + +=head1 DESCRIPTION + +mass-bug assists in filing a mass bug report in the Debian BTS on a set of +packages. For each package in the package-list file (which should list one +package per line together with an optional version number separated +from the package name by an underscore), it fills out the template, adds +BTS pseudo-headers, and either displays or sends the bug report. + +Warning: Some care has been taken to avoid unpleasant and common mistakes, +but this is still a power tool that can generate massive amounts of bug +report mails. Use it with care, and read the documentation in the +Developer's Reference about mass filing of bug reports first. + +=head1 TEMPLATE + +The template file is the body of the message that will be sent for each bug +report, excluding the BTS pseudo-headers. In the template, #PACKAGE# is +replaced with the name of the package. If a version was specified for +the package, #VERSION# will be replaced by that version. + +The components of the version number may be specified using #EPOCH#, +#UPSTREAM_VERSION# and #REVISION#. #EPOCH# includes the trailing colon and +#REVISION# the leading dash so that #EPOCH#UPSTREAM_VERSION##REVISION# is +always the same as #VERSION#. + +Note that text in the template will be automatically word-wrapped to 70 +columns, up to the start of a signature (indicated by S<'-- '> at the +start of a line on its own). This is another reason to avoid including +BTS pseudo-headers in your template. + +=head1 OPTIONS + +B<mass-bug> examines the B<devscripts> configuration files as described +below. Command line options override the configuration file settings, +though. + +=over 4 + +=item B<--severity=>(B<wishlist>|B<minor>|B<normal>|B<important>|B<serious>|B<grave>|B<critical>) + +Specify the severity with which bugs should be filed. Default +is B<normal>. + +=item B<--display> + +Fill out the templates for each package and display them all for +verification. This is the default behavior. + +=item B<--send> + +Actually send the bug reports. + +=item B<--subject=">I<bug subject>B<"> + +Specify the subject of the bug report. The subject will be automatically +prefixed with the name of the package that the bug is filed against. + +=item B<--tags> + +Set the BTS pseudo-header for tags. + +=item B<--user> + +Set the BTS pseudo-header for a usertags' user. + +=item B<--usertags> + +Set the BTS pseudo-header for usertags. + +=item B<--control=>I<COMMAND> + +Add a BTS control command. This option may be repeated to add multiple +control commands. For example, if you are mass-bug-filing "please stop +depending on this deprecated package", and bug 123456 represents removal +of the deprecated package, you could use: + + mass-bug --control='block 123456 by -1' ... + +=item B<--source> + +Specify that package names refer to source packages rather than binary +packages. + +=item B<--sendmail=>I<SENDMAILCMD> + +Specify the B<sendmail> command. The command will be split on white +space and will not be passed to a shell. Default is F</usr/sbin/sendmail>. + +=item B<--no-wrap> + +Do not wrap the template to lines of 70 characters. + +=item B<--no-conf>, B<--noconf> + +Do not read any configuration files. This can only be used as the +first option given on the command-line. + +=item B<--help> + +Provide a usage message. + +=item B<--version> + +Display version information. + +=back + +=head1 ENVIRONMENT + +B<DEBEMAIL> and B<EMAIL> can be set in the environment to control the email +address that the bugs are sent from. + +=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<BTS_SENDMAIL_COMMAND> + +If this is set, specifies a B<sendmail> command to use instead of +F</usr/sbin/sendmail>. Same as the B<--sendmail> command line option. + +=back + +=cut + +use strict; +use warnings; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use Text::Wrap; +use File::Basename; +use POSIX qw(locale_h strftime); + +setlocale(LC_TIME, "C"); # so that strftime is locale independent + +my $progname = basename($0); +$Text::Wrap::columns = 70; +my $submission_email = "maintonly\@bugs.debian.org"; +my $sendmailcmd = '/usr/sbin/sendmail'; +my $modified_conf_msg; +my %versions; + +sub usageerror { + die +"Usage: $progname [options] --subject=\"bug subject\" <template> <package-list>\n"; +} + +sub usage { + print <<"EOT"; +Usage: + $progname [options] --subject="bug subject" <template> <package-list> + +Valid options are: + --display Display the messages but don\'t send them + --send Actually send the mass bug reports to the BTS + --subject="bug subject" + Text for email subject line (will be prefixed + with "package: ") + --severity=(wishlist|minor|normal|important|serious|grave|critical) + Specify the severity of the bugs to be filed + (default "normal") + + --tags=tags Set the BTS pseudo-header for tags. + --user=user Set the BTS pseudo-header for a usertags' user + --usertags=usertags Set the BTS pseudo-header for usertags + --control="COMMAND" Add an arbitrary BTS control command (repeatable) + --source Specify that package names refer to source packages + + --sendmail=cmd Sendmail command to use (default /usr/sbin/sendmail) + --no-wrap Don't wrap the template to 70 chars. + --no-conf, --noconf Don\'t read devscripts config files; + must be the first option given + --help Display this message + --version Display version and copyright info + + <template> File containing email template; #PACKAGE# will + be replaced by the package name and #VERSION# + with the corresponding version (or a blank + string if the version was not specified) + <package-list> File containing list of packages, one per line + in the format package(_version) + + Ensure that you read the Developer\'s Reference on mass-filing bugs before + using this script! + +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 2006 by Joey Hess, 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 +} + +# Next, read 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 = ('BTS_SENDMAIL_COMMAND' => '/usr/sbin/sendmail',); + 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{'BTS_SENDMAIL_COMMAND'} =~ /./ + or $config_vars{'BTS_SENDMAIL_COMMAND'} = '/usr/sbin/sendmail'; + + if ($config_vars{'BTS_SENDMAIL_COMMAND'} ne '/usr/sbin/sendmail') { + my $cmd = (split ' ', $config_vars{'BTS_SENDMAIL_COMMAND'})[0]; + unless ($cmd =~ /^~?[A-Za-z0-9_\-\+\.\/]*$/) { + warn +"BTS_SENDMAIL_COMMAND contained funny characters: $cmd\nReverting to default value /usr/sbin/sendmail\n"; + $config_vars{'BTS_SENDMAIL_COMMAND'} = '/usr/sbin/sendmail'; + } elsif (system("command -v $cmd >/dev/null 2>&1") != 0) { + warn +"BTS_SENDMAIL_COMMAND $cmd could not be executed.\nReverting to default value /usr/sbin/sendmail\n"; + $config_vars{'BTS_SENDMAIL_COMMAND'} = '/usr/sbin/sendmail'; + } + } + + 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; + + $sendmailcmd = $config_vars{'BTS_SENDMAIL_COMMAND'}; +} + +sub gen_subject { + my $subject = shift; + my $package = shift; + + return "$package\: $subject"; +} + +sub gen_bug { + my $template_text = shift; + my $package = shift; + my $severity = shift; + my $tags = shift; + my $user = shift; + my $usertags = shift; + my $nowrap = shift; + my $type = shift; + my $control = shift; + my $version = ""; + my $bugtext; + + $version = $versions{$package} || ""; + + my ($epoch, $upstream, $revision) + = ($version =~ /^(\d+:)?(.+?)(-[^-]+)?$/); + $epoch ||= ""; + $revision ||= ""; + + $template_text =~ s/#PACKAGE#/$package/g; + $template_text =~ s/#VERSION#/$version/g; + $template_text =~ s/#EPOCH#/$epoch/g; + $template_text =~ s/#UPSTREAM_VERSION#/$upstream/g; + $template_text =~ s/#REVISION#/$revision/g; + + $version = "Version: $version\n" if $version; + + unless ($nowrap) { + if ($template_text =~ /\A(.*?)(^-- $)(.*)/ms) + { # there's a sig involved + my ($presig, $sig) = ($1, $2 . $3); + $template_text = fill("", "", $presig) . "\n" . $sig; + } else { + $template_text = fill("", "", $template_text); + } + } + if (defined $control) { + $control = join '', map { "Control: $_\n" } @$control; + } else { + $control = ''; + } + $bugtext = "$type: $package\n$version" + . "Severity: $severity\n$tags$user$usertags$control\n$template_text"; + return $bugtext; +} + +sub div { + print +("-" x 79) . "\n"; +} + +sub mailbts { + my ($subject, $body, $to, $from) = @_; + + if (defined $from) { + my $date = strftime "%a, %d %b %Y %T %z", localtime; + + my $pid = open(MAIL, "|-"); + if (!defined $pid) { + die "$progname: Couldn't fork: $!\n"; + } + $SIG{'PIPE'} = sub { die "$progname: pipe for $sendmailcmd broke\n"; }; + if ($pid) { + # parent + print MAIL <<"EOM"; +From: $from +To: $to +Subject: $subject +Date: $date +X-Generator: mass-bug from devscripts ###VERSION### + +$body +EOM + close MAIL or die "$progname: sendmail error: $!\n"; + } else { + # child + exec(split(' ', $sendmailcmd), "-t") + or die "$progname: error running sendmail: $!\n"; + } + } else { # No $from + unless (system("command -v mail >/dev/null 2>&1") == 0) { + die +"$progname: You need to either specify an email address (say using DEBEMAIL)\n or have the mailx/mailutils package installed to send mail!\n"; + } + my $pid = open(MAIL, "|-"); + if (!defined $pid) { + die "$progname: Couldn't fork: $!\n"; + } + $SIG{'PIPE'} = sub { die "$progname: pipe for mail broke\n"; }; + if ($pid) { + # parent + print MAIL $body; + close MAIL or die "$progname: error running mail: $!\n"; + } else { + # child + exec("mail", "-s", $subject, $to) + or die "$progname: error running mail: $!\n"; + } + } +} + +my $mode = "display"; +my $subject; +my $severity = "normal"; +my $tags = ""; +my $user = ""; +my $usertags = ""; +my @control = (); +my $type = "Package"; +my $opt_sendmail; +my $nowrap = ""; + +if ( + !GetOptions( + "display" => sub { $mode = "display" }, + "send" => sub { $mode = "send" }, + "subject=s" => \$subject, + "severity=s" => \$severity, + "tags=s" => \$tags, + "user=s" => \$user, + "usertags=s" => \$usertags, + "control=s" => \@control, + "source" => sub { $type = "Source"; }, + "sendmail=s" => \$opt_sendmail, + "help" => sub { usage(); exit 0; }, + "version" => sub { version(); exit 0; }, + "no-wrap" => sub { $nowrap = 1; }, + 'noconf|no-conf' => + sub { die '--noconf must come first on the command line' }, + ) +) { + usageerror(); +} + +if (!defined $subject || !length $subject) { + print STDERR + "$progname: You must specify a subject for the bug reports.\n"; + usageerror(); +} + +unless ( + $severity =~ /^(wishlist|minor|normal|important|serious|grave|critical)$/) +{ + print STDERR +"$progname: Severity must be one of wishlist, minor, normal, important, serious, grave or critical.\n"; + usageerror(); +} + +if (@ARGV != 2) { + usageerror(); +} + +if ($tags) { + $tags = "Tags: $tags\n"; +} + +if ($user) { + $user = "User: $user\n"; +} + +if ($usertags) { + $usertags = "Usertags: $usertags\n"; +} + +if ($opt_sendmail) { + if ( $opt_sendmail ne '/usr/sbin/sendmail' + and $opt_sendmail ne $sendmailcmd) { + my $cmd = (split ' ', $opt_sendmail)[0]; + unless ($cmd =~ /^~?[A-Za-z0-9_\-\+\.\/]*$/) { + warn +"--sendmail command contained funny characters: $cmd\nReverting to default value $sendmailcmd\n"; + undef $opt_sendmail; + } elsif (system("command -v $cmd >/dev/null 2>&1") != 0) { + warn +"--sendmail command $cmd could not be executed.\nReverting to default value $sendmailcmd\n"; + undef $opt_sendmail; + } + } +} +$sendmailcmd = $opt_sendmail if $opt_sendmail; + +my $template = shift; +my $package_list = shift; + +my $template_text; +open(T, "$template") || die "$progname: error reading $template: $!\n"; +{ + local $/ = undef; + $template_text = <T>; +} +close T; +if (!length $template_text) { + die "$progname: empty template\n"; +} + +my @packages; +open(L, "$package_list") || die "$progname: error reading $package_list: $!\n"; +while (<L>) { + chomp; + if (!/^([-+\.a-z0-9]+)(?:_(.*))?$/) { + die "\"$_\" does not look like the name of a Debian package\n"; + } + push @packages, $1; + $versions{$1} = $2 if $2; +} +close L; + +# Uses variables from above. +sub showsample { + my $package = shift; + + print "To: $submission_email\n"; + print "Subject: " . gen_subject($subject, $package) . "\n"; + print "\n"; + print gen_bug( + $template_text, $package, $severity, $tags, $user, + $usertags, $nowrap, $type, \@control + ) . "\n"; +} + +if ($mode eq 'display') { + print "Displaying all " . scalar(@packages) . " bug reports..\n"; + print "Run again with --send switch to send the bug reports.\n"; + div(); + foreach my $package (@packages) { + showsample($package); + div(); + } +} elsif ($mode eq 'send') { + my $from; + $from ||= $ENV{'DEBEMAIL'}; + $from ||= $ENV{'EMAIL'}; + + print "Preparing to send " + . scalar(@packages) + . " bug reports like this one:\n"; + div(); + showsample($packages[0]); + div(); + $| = 1; + print +"Are you sure that you have read the Developer's Reference on mass-filing\nbug reports, have checked this case out on debian-devel, and really want to\nsend out these " + . scalar(@packages) + . " bug reports? [yes/no] "; + my $ans = <STDIN>; + + unless ($ans =~ /^yes$/i) { + print "OK, aborting.\n"; + exit 0; + } + print "OK, going ahead then...\n"; + foreach my $package (@packages) { + print "Sending bug for $package ...\n"; + mailbts( + gen_subject($subject, $package), + gen_bug( + $template_text, $package, $severity, + $tags, $user, $usertags, + $nowrap, $type, \@control, + ), + $submission_email, + $from + ); + } + print "All bugs sent.\n"; +} + +=head1 COPYRIGHT + +This program is Copyright (C) 2006 by Joey Hess <joeyh@debian.org>. + +It is licensed under the terms of the GPL, either version 2 of the +License, or (at your option) any later version. + +=head1 AUTHOR + +Joey Hess <joeyh@debian.org> + +=cut diff --git a/scripts/mergechanges.1 b/scripts/mergechanges.1 new file mode 100644 index 0000000..f815862 --- /dev/null +++ b/scripts/mergechanges.1 @@ -0,0 +1,33 @@ +.TH MERGECHANGES 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +mergechanges \- merge multiple changes files +.SH SYNOPSIS +\fBmergechanges\fR [\fB\-d\fR] [\fB\-f\fR] [\fB\-S\fR] [\fB\-i\fR] \fIfile1 file2\fR [\fIfile\fR...] +.SH DESCRIPTION +\fBmergechanges\fR merges two or more \fI.changes\fR files, merging +the Architecture, Description and Files (and Checksums-*, if present) +fields of the two. There are checks made to ensure that the changes +files are from the same source package and version and use the same +changes file Format. The first changes file is used as the basis and +the information from the later ones is merged into it. +.PP +The output is normally written to \fIstdout\fR. If the \fB\-f\fR +option is given, the output is written to +\fIpackage\fR_\fIversion\fR_multi.changes instead, in the same +directory as the first changes file listed. +.PP +If the \fB\-d\fR option is given and the output is generated successfully, the +input files will be deleted. +.PP +If the \fB\-i\fR or \fB\-\-indep\fR option is given, source packages +and architecture-independent (Architecture: all) packages are included +in the output, but architecture-dependent packages are not. +.PP +If the \fB\-S\fR or \fB\-\-source\fR option is given, only source packages +are included in the output. +.SH AUTHOR +Gergely Nagy <algernon@debian.org>, +modifications by Julian Gilbey <jdg@debian.org>, +Mark Hymers <mhy@debian.org>, +Adam D. Barratt <adam@adam-barratt.org.uk>, and +Simon McVittie <smcv@debian.org>. diff --git a/scripts/mergechanges.sh b/scripts/mergechanges.sh new file mode 100755 index 0000000..ffaf992 --- /dev/null +++ b/scripts/mergechanges.sh @@ -0,0 +1,402 @@ +#!/bin/bash +## +## mergechanges -- merge Architecture: and Files: fields of a set of .changes +## Copyright 2002 Gergely Nagy <algernon@debian.org> +## Changes copyright 2002,2003 by Julian Gilbey <jdg@debian.org> +## +## $MadHouse: home/bin/mergechanges,v 1.1 2002/01/25 12:37:27 algernon Exp $ +## +## 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##*/} + +synopsis() { + echo "Usage: $PROGNAME [-h|--help|--version] [-d] [-S|--source] [-i|--indep] [-f] <file1> <file2> [<file> ...]" +} + +usage() { + synopsis + cat <<EOT + Merge the changes files <file1>, <file2>, .... Output on stdout + unless -f option given, in which case, output to + <package>_<version>_multi.changes in the same directory as <file1>. + If -i is given, only source and architecture-independent packages + are included in the output. + If -S is given, only the source package is included in the output. +EOT +} + +version() { + echo \ +"This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This code is copyright (C) 2002 Gergely Nagy <algernon@debian.org> +Changes copyright 2002 by 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." +} + +# Commandline parsing +FILE=0 +DELETE=0 +REMOVE_ARCHDEP=0 +REMOVE_INDEP=0 + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --version) + version + exit 0 + ;; + -f) + FILE=1 + shift + ;; + -d) + DELETE=1 + shift + ;; + -i|--indep) + REMOVE_ARCHDEP=1 + shift + ;; + -S|--source) + REMOVE_ARCHDEP=1 + REMOVE_INDEP=1 + shift + ;; + -*) + echo "Unrecognised option $1. Use $PROGNAME --help for help" >&2 + exit 1 + ;; + *) + break + ;; + esac +done + +# Sanity check #0: Do we have enough parameters? +if [ $# -lt 2 ]; then + echo "Not enough parameters." >&2 + synopsis >&2 + exit 1 +fi + +# Sanity check #1: Do the requested files exist? +for f in "$@"; do + if ! test -r $f; then + echo "ERROR: Cannot read $f!" >&2 + exit 1 + fi +done + +# Get a (possibly multi-line) field. +get_field() { + perl -e ' + use warnings; + use strict; + use autodie; + + use Dpkg::Control; + + my $field = shift; + foreach my $file (@ARGV) { + my $changes = Dpkg::Control->new(type => CTRL_FILE_CHANGES); + $changes->load($file); + next unless defined $changes->{$field}; + print $changes->{$field}; + print "\n"; + } + ' "$@" +} + +# Extract the Architecture: field from all .changes files, +# and merge them, sorting out duplicates. Skip architectures +# other than all and source if desired. +ARCHS=$(get_field Architecture "$@" | tr ' ' '\n' | sort -u) +if test ${REMOVE_ARCHDEP} = 1; then + ARCHS=$(echo "$ARCHS" | grep -E '^(all|source)$') +fi +if test ${REMOVE_INDEP} = 1; then + ARCHS=$(echo "$ARCHS" | grep -vxF all) +fi +ARCHS=$(echo "$ARCHS" | tr '\n' ' ' | sed 's/ $//') + +checksum_uniq() { + local line + local IFS= + if test ${REMOVE_ARCHDEP} = 1 -o ${REMOVE_INDEP} = 1; then + while read line; do + case "$line" in + ("") + # empty first line + echo "$line" + ;; + (*.dsc|*.diff.gz|*.tar.*|*_source.buildinfo) + # source + echo "$line" + ;; + (*_all.deb|*_all.udeb|*_all.buildinfo) + # architecture-indep + if test ${REMOVE_INDEP} = 0; then + echo "$line" + fi + ;; + (*.deb|*.udeb|*.buildinfo) + # architecture-specific + if test ${REMOVE_ARCHDEP} = 0; then + echo "$line" + fi + ;; + (*) + echo "Unrecognised file, is it architecture-dependent?" >&2 + echo "$line" >&2 + exit 1 + ;; + esac + done | awk '{if(arr[$NF] != 1){arr[$NF] = 1; print;}}' + else + awk '{if(arr[$NF] != 1){arr[$NF] = 1; print;}}' + fi +} + +# Extract & merge the Version: field from all files.. +# Don't catch Version: GnuPG lines, though! +VERSION=$(get_field Version "$@" | sort -u) +SVERSION=$(echo "$VERSION" | perl -pe 's/^\d+://') +# Extract & merge the sources from all files +SOURCE=$(get_field Source "$@" | sort -u) +# Extract & merge the files from all files +FILES=$(get_field Files "$@" | checksum_uniq) +# Extract & merge the sha1 checksums from all files +SHA1S=$(get_field Checksums-Sha1 "$@" | checksum_uniq) +# Extract & merge the sha256 checksums from all files +SHA256S=$(get_field Checksums-Sha256 "$@" | checksum_uniq) +# Extract & merge the description from all files +DESCRIPTIONS=$(get_field Description "$@" | sort -u) +# Extract & merge the Formats from all files +FORMATS=$(get_field Format "$@" | sort -u) +# Extract & merge the Checksums-* field names from all files +CHECKSUMS=$(grep -h "^Checksums-.*:" "$@" | sort -u) +UNSUPCHECKSUMS="$(echo "${CHECKSUMS}" | grep -v "^Checksums-Sha\(1\|256\):" || true)" + +# Sanity check #2: Versions must match +if test $(echo "${VERSION}" | wc -l) -ne 1; then + echo "ERROR: Version numbers do not match:" >&2 + grep "^Version: [0-9]" "$@" >&2 + exit 1 +fi + +# Sanity check #3: Sources must match +if test $(echo "${SOURCE}" | wc -l) -ne 1; then + echo "Error: Source packages do not match:" >&2 + grep "^Source: " "$@" >&2 + exit 1 +fi + +# Sanity check #4: Description for same binary must match +if test $(echo "${DESCRIPTIONS}" | sed -e 's/ \+- .*$//' | uniq -d | wc -l) -ne 0; then + echo "Error: Descriptions do not match:" >&2 + echo "${DESCRIPTIONS}" >&2 + exit 1 +fi + +# Sanity check #5: Formats must match +if test $(echo "${FORMATS}" | wc -l) -ne 1; then + if test "${FORMATS}" = "$(printf "1.7\n1.8\n")"; then + FORMATS="1.7" + CHECKSUMS="" + UNSUPCHECKSUMS="" + SHA1S="" + SHA256S="" + else + echo "Error: Changes files have different Format fields:" >&2 + grep "^Format: " "$@" >&2 + exit 1 + fi +fi + +# Sanity check #6: The Format must be one we understand +case "$FORMATS" in + 1.7|1.8) # Supported + ;; + *) + echo "Error: Changes files use unknown Format:" >&2 + echo "${FORMATS}" >&2 + exit 1 + ;; +esac + +# Sanity check #7: Unknown checksum fields +if test -n "${UNSUPCHECKSUMS}"; then + echo "Error: Unsupported checksum fields:" >&2 + echo "${UNSUPCHECKSUMS}" >&2 + exit 1 +fi + +if test ${FILE} = 1; then + DIR=$(dirname "$1") + REDIR1="> '${DIR}/${SOURCE}_${SVERSION}_multi.changes'" + REDIR2=">$REDIR1" +fi + +# Temporary output +OUTPUT=$(mktemp --tmpdir mergechanges.tmp.XXXXXXXXXX) +DESCFILE=$(mktemp --tmpdir mergechanges.tmp.XXXXXXXXXX) +trap 'rm -f "${OUTPUT}" "${DESCFILE}"' EXIT + +# Copy one of the files to ${OUTPUT}, nuking any PGP signature +if $(grep -q "BEGIN PGP SIGNED MESSAGE" "$1"); then + perl -ne 'next if 1../^$/; next if /^$/..1; print' "$1" > ${OUTPUT} +else + cp "$1" ${OUTPUT} +fi + +# Combine the Binary: and Description: fields. This is straightforward, +# unless we want to exclude some binary packages, in which case we need +# more thought. +BINARY=$(get_field Binary "$@" | tr ' ' '\n' | sort -u) +if test ${REMOVE_ARCHDEP} = 1 && test ${REMOVE_INDEP} = 1; then + BINARY= + DESCRIPTIONS= +elif test ${REMOVE_ARCHDEP} = 1 || test ${REMOVE_INDEP} = 1; then + keep_binaries=$( + get_field Files "$@" | while read -r line; do + file="${line##* }" + case "$line" in + ("") + # empty first line + echo "$line" + ;; + (*.dsc|*.diff.gz|*.tar.*|*.buildinfo) + # source or buildinfo + ;; + (*_all.deb|*_all.udeb) + # architecture-indep + package="${file%%_*}" + + if ! echo "$BINARY" | grep -q -x -F "$package"; then + echo "Warning: $package not found in Binary field" >&2 + echo "$line" >&2 + fi + + if test ${REMOVE_INDEP} != 1; then + echo "$package" + fi + ;; + (*.deb|*.udeb) + # architecture-specific + package="${file%%_*}" + + if ! echo "$BINARY" | grep -q -x -F "$package"; then + echo "Warning: $package not found in Binary field" >&2 + echo "$line" >&2 + fi + + if test ${REMOVE_ARCHDEP} != 1; then + echo "$package" + fi + ;; + (*) + echo "Unrecognised file, is it architecture-dependent?" >&2 + echo "$line" >&2 + exit 1 + ;; + esac + done \ + | tr '\n' ' ') + + BINARY=$( + echo "$BINARY" | + while read -r line; do + if echo " $keep_binaries" | grep -q -F " $line "; then + echo "$line"; + fi + done + ) + DESCRIPTIONS=$( + echo "$DESCRIPTIONS" | + while read -r line; do + package="${line%% *}" + if echo " $keep_binaries" | grep -q -F " $package "; then + echo "$line"; + fi + done + ) +fi +BINARY=$(echo "$BINARY" | tr '\n' ' ' | sed 's/ $//') + +if test -n "${DESCRIPTIONS}"; then + printf "Description:" > "${DESCFILE}" + echo "${DESCRIPTIONS}" | sed -e 's/^/ /' >> "${DESCFILE}" +fi + +if [ -n "$BINARY" ]; then + BINARY="Binary: $BINARY\\n" +fi + +# Modify the output to be the merged version: +# * Replace the Architecture: and Binary: fields +# * Nuke the value of Checksums-*: and Files: +# * Insert the Description: field before the Changes: field +# +# We print Binary directly before Source instead of directly replacing +# Binary, because with dpkg 1.19.3, if the first .changes file is +# source-only, it won't have a Binary field at all. +eval "awk -- '/^[^ ]/{ deleting=0 } + /^ /{ + if (!deleting) { + print + } + next + } + /^Architecture: /{printf \"%s ${ARCHS}\\n\", \$1; deleting=1; next} + /^Source: /{printf \"${BINARY}\"; print; next} + /^Binary: /{deleting=1; next} + /^Changes:/{ + field=\$0 + while ((getline < \"${DESCFILE}\") > 0) { + print + } + printf \"%s\\n\", field + next + } + /^Format: /{ printf \"%s ${FORMATS}\\n\", \$1; deleting=1; next} + /^(Checksums-.*|Files|Description):/{ deleting=1; next } + { print }' \ + ${OUTPUT} ${REDIR1}" + +# Voodoo magic to get the merged file and checksum lists into the output +if test -n "${SHA1S}"; then + eval "printf 'Checksums-Sha1:' ${REDIR2}" + eval "echo '${SHA1S}' | sed -e 's/^/ /' ${REDIR2}" +fi +if test -n "${SHA256S}"; then + eval "printf 'Checksums-Sha256:' ${REDIR2}" + eval "echo '${SHA256S}' | sed -e 's/^/ /' ${REDIR2}" +fi +eval "printf 'Files:' ${REDIR2}" +eval "echo '${FILES}' | sed -e 's/^/ /' ${REDIR2}" + +if test ${DELETE} = 1; then + rm "$@" +fi + +exit 0 diff --git a/scripts/mk-build-deps.pl b/scripts/mk-build-deps.pl new file mode 100755 index 0000000..b1b4535 --- /dev/null +++ b/scripts/mk-build-deps.pl @@ -0,0 +1,616 @@ +#!/usr/bin/perl + +# mk-build-deps: make a dummy package to satisfy build-deps of a package +# Copyright 2008 by Vincent Fourmond +# +# 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 + +# Changes: +# * (Vincent Fourmond 4/4/2008): now take Build-Depends-Indep +# into consideration + +=head1 NAME + +mk-build-deps - build a package satisfying a package's build-dependencies + +=head1 SYNOPSIS + +B<mk-build-deps> B<--help>|B<--version> + +B<mk-build-deps> [I<options>] I<control file> | I<package name> ... + +=head1 DESCRIPTION + +Given a I<package name> and/or I<control file>, B<mk-build-deps> +will use B<equivs> to generate a binary package which may be installed to +satisfy all the build dependencies of the given package. + +If B<--build-dep> and/or B<--build-indep> are given, then the resulting binary +package(s) will depend solely on the Build-Depends/Build-Depends-Indep +dependencies, respectively. + +=head1 OPTIONS + +=over 4 + +=item B<-i>, B<--install> + +Install the generated packages and its build-dependencies. + +=item B<-t>, B<--tool> + +When installing the generated package use the specified tool. +(default: B<apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends>) + +=item B<-r>, B<--remove> + +Remove the package file after installing it. Ignored if used without +the B<--install> switch. + +=item B<-a> I<foo>, B<--arch> I<foo> + +Set the architecture of the produced binary package to I<foo>. If this option +is not given, fall back to the value given by B<--host-arch>. If neither this +option nor B<--host-arch> are given but the Build-Depends contain architecture +restrictions, use the value printed by `dpkg-architecture -qDEB_HOST_ARCH`. +Otherwise, use I<all>. + +The package architecture must be equal to the host architecture except if the +package architecture is I<all>. + +The package architecture cannot be I<all> if the build and host architecture +differ. + +=item B<--host-arch> I<foo> + +Set the host architecture the binary package is built for. This defaults to +the value printed by `dpkg-architecture -qDEB_HOST_ARCH`. Use this option to +create a binary package that is able to satisfy crossbuild dependencies. + +If this option is used together with B<--arch>, then they must be equal except +if the value of B<--arch> is I<all>. + +If B<--arch> is not given, then this option also sets the package architecture. + +=item B<--build-arch> I<foo> + +Set the build architecture the binary package is built for. This defaults to +the value printed by `dpkg-architecture -qDEB_BUILD_ARCH`. Use this option to +create a binary package that is able to satisfy crossbuild dependencies. + +=item B<-B>, B<--build-dep> + +Generate a package which only depends on the source package's Build-Depends +dependencies. + +=item B<-A>, B<--build-indep> + +Generate a package which only depends on the source package's +Build-Depends-Indep dependencies. + +=item B<-P>, B<--build-profiles> I<profile[,...]> + +Generate a package which only depends on build dependencies +with the build profile(s), given as a comma-separated list. +The default behavior is to use no specific profile. +Setting this option will override the B<DEB_BUILD_PROFILES> +environment variable. + +=item B<-h>, B<--help> + +Show a summary of options. + +=item B<-v>, B<--version> + +Show version and copyright information. + +=item B<-s>, B<--root-cmd> + +Use the specified tool to gain root privileges before installing. +Ignored if used without the B<--install> switch. + +=back + +=head1 ENVIRONMENT + +=head2 External environment + +=over 4 + +=item B<DEB_BUILD_PROFILES> + +If set, it will be used as the active build profile(s) for the +build dependencies to be installed. +It is a space separated list of profile names. +Overridden by the B<-P> option. + +=back + +=head1 AUTHOR + +B<mk-build-deps> is copyright by Vincent Fourmond and was modified for the +devscripts package by Adam D. Barratt <adam@adam-barratt.org.uk>. + +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. + +=cut + +use 5.01; +use strict; +use warnings; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use File::Basename; +use Pod::Usage; +use Dpkg::Control; +use Dpkg::Version; +use Dpkg::IPC; +use Dpkg::Deps; +use FileHandle; +use Text::ParseWords; + +my $progname = basename($0); +my $opt_install; +my $opt_remove = 0; +my ($opt_help, $opt_version, $opt_arch, $opt_dep, $opt_indep, $opt_hostarch, + $opt_buildarch, $opt_buildprofiles); +my $control; +my $install_tool; +my $root_cmd; +my @packages; + +my @config_files = ('/etc/devscripts.conf', '~/.devscripts'); +my %config_vars = ( + 'MKBUILDDEPS_TOOL' => +'/usr/bin/apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends', + 'MKBUILDDEPS_REMOVE_AFTER_INSTALL' => 'no', + 'MKBUILDDEPS_ROOTCMD' => '', +); +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{'MKBUILDDEPS_TOOL'} =~ /./ + or $config_vars{'MKBUILDDEPS_TOOL'} = $config_default{'MKBUILDDEPS_TOOL'}; +$config_vars{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'} =~ /^(yes|no)$/ + or $config_vars{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'} + = $config_default{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'}; +$config_vars{'MKBUILDDEPS_ROOTCMD'} =~ /./ + or $config_vars{'MKBUILDDEPS_ROOTCMD'} + = $config_default{'MKBUILDDEPS_ROOTCMD'}; + +$install_tool = $config_vars{'MKBUILDDEPS_TOOL'}; +$root_cmd = $config_vars{'MKBUILDDEPS_ROOTCMD'}; + +if ($config_vars{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'} =~ /yes/) { + $opt_remove = 1; +} + +sub usage { + my ($exitval) = @_; + + my $verbose = $exitval ? 0 : 1; + pod2usage({ -exitval => 'NOEXIT', -verbose => $verbose }); + + if ($verbose) { + my $modified_conf_msg; + 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; + print <<EOF; +Default settings modified by devscripts configuration files: +$modified_conf_msg +EOF + } + + exit $exitval; +} + +GetOptions( + "help|h" => \$opt_help, + "version|v" => \$opt_version, + "install|i" => \$opt_install, + "remove|r" => \$opt_remove, + "tool|t=s" => \$install_tool, + "arch|a=s" => \$opt_arch, + "host-arch=s" => \$opt_hostarch, + "build-arch=s" => \$opt_buildarch, + "build-dep|B" => \$opt_dep, + "build-indep|A" => \$opt_indep, + "build-profiles|P=s" => \$opt_buildprofiles, + "root-cmd|s=s" => \$root_cmd, +) or usage(1); + +usage(0) if ($opt_help); + +if ($opt_version) { version(); exit 0; } + +if (!@ARGV) { + if (-r 'debian/control') { + push(@ARGV, 'debian/control'); + } +} + +usage(1) unless @ARGV; + +system("command -v equivs-build >/dev/null 2>&1"); +if ($?) { + die "$progname: You must have equivs installed to use this program.\n"; +} + +while ($control = shift) { + my ($name, $fh, $descr, $pid); + if (-r $control and -f $control) { + open $fh, $control; + unless (defined $fh) { + warn "Unable to open $control: $!\n"; + next; + } + $name = 'Source'; + $descr = "control file `$control'"; + } else { + $fh = FileHandle->new(); + $pid = spawn( + exec => ['apt-cache', 'showsrc', $control], + from_file => '/dev/null', + to_pipe => $fh + ); + unless (defined $pid) { + warn "Unable to run apt-cache: $!\n"; + next; + } + $name = 'Package'; + $descr = "`apt-cache showsrc $control'"; + } + + my (@pkgInfo, @versions); + until (eof $fh) { + my $ctrl = Dpkg::Control->new(allow_pgp => 1, type => CTRL_UNKNOWN); + # parse() dies if the file isn't syntactically valid and returns undef + # if there simply weren't any fields parsed + unless ($ctrl->parse($fh, $descr)) { + warn "$progname: Unable to find package name in $descr\n"; + next; + } + unless (exists $ctrl->{$name}) { + next; + } + my $args = ''; + my $arch = 'all'; + my ($build_deps, $build_dep, $build_dep_arch, $build_indep); + my ($build_conflicts, $build_conflict, $conflict_arch, + $conflict_indep); + + if (exists $ctrl->{'Build-Depends'}) { + $build_dep = $ctrl->{'Build-Depends'}; + $build_dep =~ s/\n/ /g; + $build_deps = $build_dep; + } + if (exists $ctrl->{'Build-Depends-Arch'}) { + $build_dep_arch = $ctrl->{'Build-Depends-Arch'}; + $build_dep_arch =~ s/\n/ /g; + $build_dep .= ', ' if $build_dep; + $build_dep .= $build_dep_arch; + $build_deps .= ', ' if $build_deps; + $build_deps .= $build_dep_arch; + } + if (exists $ctrl->{'Build-Depends-Indep'}) { + $build_indep = $ctrl->{'Build-Depends-Indep'}; + $build_indep =~ s/\n/ /g; + $build_deps .= ', ' if $build_deps; + $build_deps .= $build_indep; + } + if (exists $ctrl->{'Build-Conflicts'}) { + $build_conflict = $ctrl->{'Build-Conflicts'}; + $build_conflict =~ s/\n/ /g; + $build_conflicts = $build_conflict; + } + if (exists $ctrl->{'Build-Conflicts-Arch'}) { + $conflict_arch = $ctrl->{'Build-Conflicts-Arch'}; + $conflict_arch =~ s/\n/ /g; + $build_conflict .= ', ' if $build_conflict; + $build_conflict .= $conflict_arch; + $build_conflicts .= ', ' if $build_conflicts; + $build_conflicts .= $conflict_arch; + } + if (exists $ctrl->{'Build-Conflicts-Indep'}) { + $conflict_indep = $ctrl->{'Build-Conflicts-Indep'}; + $conflict_indep =~ s/\n/ /g; + $build_conflicts .= ', ' if $build_conflicts; + $build_conflicts .= $conflict_indep; + } + + warn "$progname: Unable to find build-deps for $ctrl->{$name}\n" + unless $build_deps; + + if (exists $ctrl->{Version}) { + push(@versions, $ctrl->{Version}); + } elsif ($name eq 'Source') { + (my $changelog = $control) =~ s@control$@changelog@; + if (-f $changelog) { + require Dpkg::Changelog::Parse; + my $log = Dpkg::Changelog::Parse::changelog_parse( + file => $changelog); + if ($ctrl->{$name} eq $log->{$name}) { + $ctrl->{Version} = $log->{Version}; + push(@versions, $log->{Version}); + } + } + } + + # Only build a package with both B-D and B-D-I in Depends if the + # B-D/B-D-I specific packages weren't requested + if (!($opt_dep || $opt_indep)) { + push( + @pkgInfo, + { + depends => $build_deps, + conflicts => $build_conflicts, + name => $ctrl->{$name}, + type => 'build-deps', + version => $ctrl->{Version} }); + next; + } + if ($opt_dep) { + push( + @pkgInfo, + { + depends => $build_dep, + conflicts => $build_conflict, + name => $ctrl->{$name}, + type => 'build-deps-depends', + version => $ctrl->{Version} }); + } + if ($opt_indep) { + push( + @pkgInfo, + { + depends => $build_indep, + conflicts => $conflict_indep, + name => $ctrl->{$name}, + type => 'build-deps-indep', + version => $ctrl->{Version} }); + } + } + wait_child($pid, nocheck => 1) if defined $pid; + # Only use the newest version. We'll only have this if processing showsrc + # output or a dsc file. + if (@versions) { + @versions = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [$_, Dpkg::Version->new($_)] } @versions; + push(@packages, + map { build_equiv($_) } + grep { $versions[0] eq $_->{version} } @pkgInfo); + } elsif (@pkgInfo) { + push(@packages, build_equiv($pkgInfo[0])); + } else { + die "$progname: Unable to find package name in $descr\n"; + } +} + +if ($opt_install) { + my @root; + if ($root_cmd) { + push(@root, shellwords($root_cmd)); + } + + my (@pkg_names, @deb_files, @buildinfo_files, @changes_files, %uniq); + for my $package (@packages) { + if ($uniq{ $package->{deb_file} }++ == 0) { + push @pkg_names, $package->{package}; + push @deb_files, $package->{deb_file}; + push @buildinfo_files, $package->{buildinfo_file}; + push @changes_files, $package->{changes_file}; + } + } + + system @root, 'dpkg', '--unpack', @deb_files; + die("$progname: dpkg --unpack failed\n") if (($? >> 8) != 0); + system @root, shellwords($install_tool), '-f', 'install'; + my $err = $? >> 8; + if (!$err) { + # $install_tool succeeded. Did the packages get installed? It's + # possible that they didn't because $install_tool may have realized + # that installation was impossible, and it could have given up, + # successfully. + for (my $i = 0 ; $i < @pkg_names ; $i++) { + my $pkg = $pkg_names[$i]; + my $status; + spawn( + exec => + ['dpkg-query', '-W', '-f', '${db:Status-Status}', $pkg], + to_string => \$status, + error_to_file => '/dev/null', + nocheck => 1, + wait_child => 1 + ); + if ($status ne 'installed' || ($? >> 8)) { + # Restore system to previous state, since $install_tool wasn't + # able to resolve a proper way to get the build-dep packages + # installed + warn "$progname: Unable to install $pkg"; + $err = 1; + } elsif ($opt_remove) { + unlink $deb_files[$i]; + unlink $buildinfo_files[$i]; + unlink $changes_files[$i]; + } + } + if ($err) { + die "$progname: Unable to install all build-dep packages\n"; + } + } else { + # Restore system to previous state, since $install_tool wasn't able to + # resolve a proper way to get the build-dep packages installed + system @root, 'dpkg', '--remove', @pkg_names; + die("$progname: Unable to install all build-dep packages\n"); + } +} + +sub version { + print <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +Copyright (C) 2008 Vincent Fourmond + +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 +} + +sub build_equiv { + my ($opts) = @_; + my $args = ''; + + my $packagearch = 'all'; + + if (defined $opt_arch) { + $packagearch = $opt_arch; + } elsif (defined $opt_hostarch) { + $packagearch = $opt_hostarch; + } elsif ($opts->{depends} =~ m/\[|\]/) { + chomp($packagearch = `dpkg-architecture -qDEB_HOST_ARCH`); + } + if ($packagearch ne "all") { + $args .= "--arch=$packagearch "; + } + + chomp(my $buildarch = `dpkg-architecture -qDEB_BUILD_ARCH`); + if (defined $opt_buildarch) { + $buildarch = $opt_buildarch; + } + + chomp(my $hostarch = `dpkg-architecture -qDEB_HOST_ARCH`); + if (defined $opt_hostarch) { + $hostarch = $opt_hostarch; + } + + if ($packagearch eq "all") { + if ($buildarch ne $hostarch) { + die +"build architecture \"$buildarch\" is unequal host architecture \"$hostarch\" in which case the package architecture must not be \"all\" (but \"$hostarch\" instead)\n"; + } + } elsif ($packagearch ne $hostarch) { + die +"The package architecture must be equal to the host architecture except if the package architecture is \"all\"\n"; + } + + my $build_profiles = [split /\s+/, ($ENV{'DEB_BUILD_PROFILES'} // "")]; + if (defined $opt_buildprofiles) { + $build_profiles = [split /,/, $opt_buildprofiles]; + } + + my $positive = deps_parse( + $opts->{depends} // "", + reduce_arch => 1, + host_arch => $hostarch, + build_arch => $buildarch, + build_dep => 1, + reduce_profiles => 1, + build_profiles => $build_profiles + ); + my $negative = deps_parse( + $opts->{conflicts} // "", + reduce_arch => 1, + host_arch => $hostarch, + build_arch => $buildarch, + build_dep => 1, + union => 1, + reduce_profiles => 1, + build_profiles => $build_profiles + ); + + # either remove :native for native builds or replace it by the build + # architecture + my $handle_native_archqual = sub { + my ($dep) = @_; + if ($dep->{archqual} && $dep->{archqual} eq "native") { + if ($hostarch eq $buildarch) { + $dep->{archqual} = undef; + } else { + $dep->{archqual} = $buildarch; + } + } + return 1; + }; + deps_iterate($positive, $handle_native_archqual); + deps_iterate($negative, $handle_native_archqual); + + my $pkgname; + my $buildess = "build-essential:$buildarch"; + if ($buildarch eq $hostarch) { + $pkgname = "$opts->{name}-$opts->{type}"; + } else { + $pkgname = "$opts->{name}-cross-$opts->{type}"; + $buildess .= ", crossbuild-essential-$hostarch:$buildarch"; + } + + my $readme = '/usr/share/devscripts/templates/README.mk-build-deps'; + open EQUIVS, "| equivs-build $args-" + or die "$progname: Failed to execute equivs-build: $!\n"; + print EQUIVS "Section: devel\n" + . "Priority: optional\n" + . "Standards-Version: 3.7.3\n\n" + . "Package: $pkgname\n" + . "Architecture: $packagearch\n" + . "Depends: $buildess, $positive\n"; + + print EQUIVS "Conflicts: $negative\n" if $negative; + + # Allow the file not to exist to ease testing + print EQUIVS "Readme: $readme\n" if -r $readme; + + my $version = '1.0'; + if (defined $opts->{version}) { + $version = $opts->{version}; + } + print EQUIVS "Version: $version\n"; + + print EQUIVS "Description: build-dependencies for $opts->{name}\n" + . " Dependency package to build the '$opts->{name}' package\n"; + + close EQUIVS; + + my $v = Dpkg::Version->new($version); + # The version in the .deb filename will not contain the epoch + $version = $v->as_string(omit_epoch => 1); + my $deb_file = "${pkgname}_${version}_${packagearch}.deb"; + my $buildinfo_file = "${pkgname}_${version}_${hostarch}.buildinfo"; + my $changes_file = "${pkgname}_${version}_${hostarch}.changes"; + return { + package => $pkgname, + deb_file => $deb_file, + buildinfo_file => $buildinfo_file, + changes_file => $changes_file, + }; +} diff --git a/scripts/mk-origtargz.bash_completion b/scripts/mk-origtargz.bash_completion new file mode 100644 index 0000000..d198ba5 --- /dev/null +++ b/scripts/mk-origtargz.bash_completion @@ -0,0 +1,49 @@ +# /usr/share/bash-completion/completions/mk-origtargz +# Bash command completion for ‘mk-origtargz(1)’. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +shopt -s progcomp + +_mk_origtargz_completion () { + COMPREPLY=() + + local cur="${COMP_WORDS[COMP_CWORD]}" + local prev="${COMP_WORDS[COMP_CWORD-1]}" + + local opts="--help --verbose --version -v" + opts+=" --exclude-file --copyright-file" + opts+=" --package --directory -C" + opts+=" --copy --symlink --rename --repack --repack-suffix -S" + opts+=" --compression --copyright-file --unzipopt" + + case "${prev}" in + --compression) + local formats=(gzip bzip2 lzma xz) + COMPREPLY=( $(compgen -W "${formats[*]}" -- "${cur}" ) ) + ;; + + --directory|-C) + COMPREPLY=( $(compgen -A directory -- "${cur}" ) ) + ;; + + --copyright-file) + COMPREPLY=( $(compgen -A file -- "${cur}" ) ) + ;; + --unzipopt) + COMPREPLY=( $(compgen -W '-Z -a -b -D -j -n' -- "${cur}" ) ) + ;; + + *) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}" ) ) + ;; + esac +} + +complete -F _mk_origtargz_completion mk-origtargz + + +# Local variables: +# coding: utf-8 +# mode: shell-script +# End: +# vim: fileencoding=utf-8 filetype=sh : diff --git a/scripts/mk-origtargz.pl b/scripts/mk-origtargz.pl new file mode 100755 index 0000000..4c2ff00 --- /dev/null +++ b/scripts/mk-origtargz.pl @@ -0,0 +1,222 @@ +#!/usr/bin/perl +# vim: set ai shiftwidth=4 tabstop=4 expandtab: +# +# mk-origtargz: Rename upstream tarball, optionally changing the compression +# and removing unwanted files. +# Copyright (C) 2014 Joachim Breitner <nomeata@debian.org> +# Copyright (C) 2015 James McCoy <jamessan@debian.org> +# +# It contains code formerly found in uscan. +# Copyright (C) 2002-2006, 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/>. + +=head1 NAME + +mk-origtargz - rename upstream tarball, optionally changing the compression and removing unwanted files + +=head1 SYNOPSIS + +=over + +=item B<mk-origtargz> [I<options>] F<foo-1.0.tar.gz> + +=item B<mk-origtargz> B<--help> + +=back + +=head1 DESCRIPTION + +B<mk-origtargz> renames the given file to match what is expected by +B<dpkg-buildpackage>, based on the source package name and version in +F<debian/changelog>. It can convert B<zip> to B<tar>, optionally change the +compression scheme and remove files according to B<Files-Excluded> and +B<Files-Excluded->I<component> in F<debian/copyright>. The resulting file is +placed in F<debian/../..>. (In F<debian/copyright>, the B<Files-Excluded> and +B<Files-Excluded->I<component> stanzas are a part of the first paragraph and +there is a blank line before the following paragraphs which contain B<Files> +and other stanzas. The B<Files-Included> stanza may be used to ignore +parts of subdirectories specified by the B<Files-Excluded> stanza See +B<uscan>(1) "COPYRIGHT FILE EXAMPLE".) + +The archive type for B<zip> is detected by "B<file --dereference --brief +--mime-type>" command. So any B<zip> type archives such as B<jar> and B<xpi> +are treated in the same way. + +If the package name is given via the B<--package> option, no information is +read from F<debian/>, and the result file is placed in the current directory. + +B<mk-origtargz> is commonly called via B<uscan>, which first obtains the +upstream tarball. + +=head1 OPTIONS + +=head2 Metadata options + +The following options extend or replace information taken from F<debian/>. + +=over + +=item B<--package> I<package> + +Use I<package> as the name of the Debian source package, and do not require or +use a F<debian/> directory. This option can only be used together with +B<--version>. + +The default is to use the package name of the first entry in F<debian/changelog>. + +=item B<-v>, B<--version> I<version> + +Use I<version> as the version of the package. This needs to be the upstream +version portion of a full Debian version, i.e. no Debian revision, no epoch. + +The default is to use the upstream portion of the version of the first entry in +F<debian/changelog>. + +=item B<--exclude-file> I<glob> + +Remove files matching the given I<glob> from the tarball, as if it was listed in +B<Files-Excluded>. + +=item B<--copyright-file> I<filename> + +Remove files matching the patterns found in I<filename>, which should have the +format of a Debian F<copyright> file +(B<Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/> +to be precise). Errors parsing that file are silently ignored, exactly as is +the case with F<debian/copyright>. + +Unmatched patterns will emit a warning so the user can verify whether it is +correct. If there are multiple patterns which match a file, only the last one +will count as being matched. + +Both the B<--exclude-file> and B<--copyright-file> options amend the list of +patterns found in F<debian/copyright>. If you do not want to read that file, +you will have to use B<--package>. + +=item B<--signature> I<signature-mode> + +Set I<signature-mode>: + +=over + +=item 0 for no signature + +=item 1 for normal detached signature + +=item 2 for signature on decompressed + +=item 3 for self signature + +=back + +=item B<--signature-file> I<signature-file> + +Use I<signature-file> as the signature file corresponding to the Debian source +package to create a B<dpkg-source> (post-stretch) compatible signature file. +(optional) + +=back + +=head2 Action options + +These options specify what exactly B<mk-origtargz> should do. The options +B<--copy>, B<--rename> and B<--symlink> are mutually exclusive. + +=over + +=item B<--symlink> + +Make the resulting file a symlink to the given original file. (This is the +default behaviour.) + +If the file has to be modified (because it is a B<zip>, B<xpi> or B<zst> file, +because of B<--repack> or B<Files-Excluded>), this option behaves like +B<--copy>. + +=item B<--copy> + +Make the resulting file a copy of the original file (unless it has to be +modified, of course). + +=item B<--rename> + +Rename the original file. + +If the file has to be modified (because it is a B<zip>, B<xpi>, B<zst> file, +because of B<--repack> or B<Files-Excluded>), this implies that the original +file is deleted afterwards. + +=item B<--repack> + +If the given file is not compressed using the desired format (see +B<--compression>), recompress it. + +=item B<-S>, B<--repack-suffix> I<suffix> + +If the file has to be modified, because of B<Files-Excluded>, append I<suffix> +to the upstream version. + +=item B<--force-repack> + +Recompress even if file is compressed using the desired format and no files +were deleted. + +=item B<-c>, B<--component> I<componentname> + +Use <componentname> as the component name for the secondary upstream tarball. +Set I<componentname> as the component name. This is used only for the +secondary upstream tarball of the Debian source package. +Then I<packagename_version.orig-componentname.tar.gz> is created. + +=item B<--compression> [ B<gzip> | B<bzip2> | B<lzma> | B<xz> | B<default> ] + +The default method is B<xz>. When mk-origtargz is launched in a debian source +repository which format is "1.0" or undefined, the method switches to B<gzip>. + +=item B<-C>, B<--directory> I<directory> + +Put the resulting file in the given directory. + +=item B<--unzipopt> I<options> + +Add the extra options to use with the B<unzip> command such as B<-a>, B<-aa>, +and B<-b>. + +=back + +=cut + +#=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: + +=head1 SEE ALSO + +B<uscan>(1), B<uupdate>(1) + +=head1 AUTHOR + +B<mk-origtargz> and this manpage have been written by Joachim Breitner +<I<nomeata@debian.org>>. + +=cut + +use Devscripts::MkOrigtargz; + +exit Devscripts::MkOrigtargz->new->do; diff --git a/scripts/namecheck.pl b/scripts/namecheck.pl new file mode 100755 index 0000000..a171513 --- /dev/null +++ b/scripts/namecheck.pl @@ -0,0 +1,231 @@ +#!/usr/bin/perl + +=head1 NAME + +namecheck - Check project names are not already taken. + +=head1 ABOUT + +This is a simple tool to automate the testing of project names at the most +common Open Source / Free Software hosting environments. + +Each new project requires a name, and those names are ideally unique. To come +up with names is hard, and testing to ensure they're not already in use is +time-consuming - unless you have a tool such as this one. + +=head1 CUSTOMIZATION + +The script, as is, contains a list of sites, and patterns, to test against. + +If those patterns aren't sufficient then you may create your own additions and +add them to the script. If you wish to have your own version of the patterns +you may save them into the file ~/.namecheckrc + +=head1 AUTHOR + +Steve +-- +http://www.steve.org.uk/ + +=head1 LICENSE + +Copyright (c) 2008 by Steve Kemp. All rights reserved. + +This module is free software; you can redistribute it and/or modify it under +the same terms as Perl itself. + +=cut + +# +# Good practise. +# +use strict; +use warnings; + +# +# A module for fetching webpages. +# +use LWP::UserAgent; + +# +# A module for finding the user home dir. +# +use File::HomeDir; + +# +# Get the name from the command line. +# +my $name = shift; +if (!defined($name)) { + print <<EOF; +Usage: $0 name +EOF + exit; +} + +# +# Get the patterns we're going to use for testing. +# +my @lines = loadPatterns(); + +# +# Assuming we have patterns use them. +# +testSites(@lines); + +# +# NOT REACHED. +# +exit; + +# +# Load the list of sites, and patterns, to test. +# +# By default these will come from the end of the script +# itself. A user may create the file ~/.namecheckrc with +# their own patterns if they prefer. +# + +sub loadPatterns { + my $file = File::HomeDir->my_home . "/.namecheckrc"; + my @lines = (); + + if (-e $file) { + open(FILE, "<", $file) + or die "Failed to open $file - $!"; + while (<FILE>) { + push(@lines, $_); + } + close(FILE); + } else { + while (<DATA>) { + push(@lines, $_); + } + } + + return (@lines); +} + +# +# Test the given name against the patterns we've loaded from our +# own script, or the users configuration file. +# + +sub testSites { + my (@patterns) = (@_); + + # + # Create and setup an agent for the downloading. + # + my $ua = LWP::UserAgent->new(); + $ua->agent('Mozilla/5.0'); + $ua->timeout(10); + $ua->env_proxy(); + + my $headers = HTTP::Headers->new(); + $headers->header('Accept' => '*/*'); + + foreach my $entry (@patterns) { + + # + # Skip blank lines, and comments. + # + chomp($entry); + next if ((!$entry) || (!length($entry))); + next if ($entry =~ /^#/); + + # + # Each line is an URL + a pattern, separated by a pipe. + # + my ($url, $pattern) = split(/\|/, $entry); + + # + # Strip leading/trailing spaces. + # + $pattern =~ s/^\s+//; + $pattern =~ s/\s+$//; + + # + # Interpolate the proposed project name in the string. + # + $url =~ s/\%s/$name/g if ($url =~ /\%s/); + + # + # Find the hostname we're downloading; just to show the user + # something is happening. + # + my $urlname = $url; + if ($urlname =~ /:\/\/([^\/]+)\//) { + $urlname = $1; + } + print sprintf "Testing %20s", $urlname; + + # + # Get the URL + # + my $request = HTTP::Request->new('GET', $url, $headers); + my $response = $ua->request($request); + + # + # If success we look at the returned text. + # + if ($response->is_success()) { + + # + # Get the page content - collapsing linefeeds. + # + my $c = $response->content(); + $c =~ s/[\r\n]//g; + + # + # Does the page have the pattern? + # + if ($c !~ /\Q$pattern\E/i) { + print " - In use\n"; + print "Aborting - name '$name' is currently used.\n"; + exit 0; + } else { + print " - Available\n"; + } + } else { + + # + # Otherwise we'll assume that 404 means that the + # project isn't taken. + # + my $c = $response->status_line(); + if ($c =~ /404/) { + print " - Available\n"; + } else { + + # + # Other errors we can't handle. + # + print "ERROR fetching $url - $c\n"; + } + } + + } + + # + # If we got here the name is free. + # + print "\n\nThe name '$name' doesn't appear to be in use.\n"; + exit 1; +} + +__DATA__ + +# +# The default patterns. +# +# If you want to customise them either do so here, or create the +# file ~/.namecheckrc with your own contents in the same format. +# +http://%s.tuxfamily.org/ | Not Found +http://freshmeat.net/projects/%s | We encounted an error +http://launchpad.net/%s | no page with this address +http://savannah.gnu.org/projects/%s | Invalid Group +http://sourceforge.net/projects/%s | Invalid Project +http://www.openhub.net/projects/%s | Something seems wrong with your URL +http://projects.apache.org/projects/%s.html | Not Found diff --git a/scripts/nmudiff.1 b/scripts/nmudiff.1 new file mode 100644 index 0000000..84f4bc8 --- /dev/null +++ b/scripts/nmudiff.1 @@ -0,0 +1,129 @@ +.TH NMUDIFF 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +nmudiff \- email an NMU diff to the Debian BTS +.SH SYNOPSIS +\fBnmudiff\fR [\fIoptions\fR] +.SH DESCRIPTION +\fBnmudiff\fR is the tool to be used while preparing a Non-Maintainer Upload +(NMU) to notify the maintainer about the work being done. +.PP +\fBnmudiff\fR should be run in the source tree of the package being +NMUed, after the NMU is built. It assumes that the source packages +(specifically, the \fI.dsc\fR and any corresponding \fItar\fR and \fIdiff\fR files) for +both the previous version of the package and the newly built NMU +version are in the parent directory. It then uses \fBdebdiff\fR to +generate a diff between the previous version and the current NMU, and +either runs mutt or an editor (using \fBsensible\-editor\fR) so that +the mail message (including the diff) can be examined and modified; +once you exit the editor the diff will be mailed to the Debian BTS. +.PP +The default behaviour is that if exactly one bug is closed by this NMU, +then that bug will be mailed, otherwise a new bug will be submitted. +This behaviour may be changed by command line options and +configuration file options. +.SH OPTIONS +.TP +.B \-\-new +Instead of mailing the bug reports which are to be closed by this NMU, +a new bug report is submitted directly to the BTS. +.TP +.B \-\-old +Send the bug report to all of the bugs which are being closed by this +NMU, rather than opening a new bug report. This option has no effect +if no bugs are being closed by this NMU. +.TP +\fB\-\-mutt\fR +Use \fBmutt\fR(1) (or \fBneomutt\fR(1)) for editing and sending the message to +the BTS (default behaviour). This can be controlled using a configuration +file option (see below). +.TP +\fB\-\-no\-mutt\fR +Use \fBsensible\-editor\fR(1) to edit the message and then mail it +directly using \fI/usr/bin/sendmail\fR. This can be controlled using +a configuration file option (see below). +.TP +\fB\-\-sendmail\fR \fISENDMAILCMD\fR +Specify the \fBsendmail\fR command. The command will be split on white +space and will be interpreted by the shell. Default is +\fI/usr/sbin/sendmail\fR. The \fB\-t\fR option will be automatically +added if the command is \fI/usr/sbin/sendmail\fR or +\fI/usr/sbin/exim*\fR. For other mailers, if they require a \fB\-t\fR +option, this must be included in the \fISENDMAILCMD\fR, for example: +\fB\-\-sendmail="/usr/sbin/mymailer \-t"\fR. This can also be set using the +devscripts configuration files; see below. +.TP +\fB\-\-from\fR \fIEMAIL\fR +If using the \fBsendmail\fR (\fB\-\-no\-mutt\fR) option, then the email to the +BTS will be sent using the name and address in the environment +variables \fBDEBEMAIL\fR and \fBDEBFULLNAME\fR. If these are not set, then the +variables \fBEMAIL\fR and \fBNAME\fR will be used instead. These can be overridden +using the \fB\-\-from\fR option. The program will not work in this case +if an email address cannot be determined. +.TP +\fB\-\-delay\fR \fIDELAY\fR +Indicate in the generated mail that the NMU has been uploaded to the +DELAYED queue, with a delay of \fIDELAY\fR days. The default value is +\fIXX\fR which adds a placeholder to the e-mail. A value of \fB0\fR indicates +that the upload has not been sent to a delayed queue. This can also be set using the +devscripts configuration files; see below. +.TP +\fB\-\-no\-delay\fR, \fB\-\-nodelay\fR +Equivalent to \fB\-\-delay 0\fR. +.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\-\-no\-pending\fR, \fB\-\-nopending\fR +Do not add the \fIpending\fR tag. +.TP +\fB\-\-non\-dd\fR, \fB\-\-nondd\fR +Mention in the email that you require sponsorship. +.TP +\fB\-\-template\fR \fITEMPLATEFILE\fR +Use content of TEMPLATEFILE for message body instead of default template. +If TEMPLATEFILE does not exist, default template is applied. +.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 NMUDIFF_DELAY +If this is set to a number, e-mails generated by \fBnmudiff\fR will by +default mention an upload to the DELAYED queue, delayed for the +specified number of days. The value \fB0\fR indicates that the DELAYED +queue has not been used. +.TP +.B NMUDIFF_MUTT +Can be \fIyes\fR (default) or \fIno\fR, and specifies whether to use +\fBmutt\fR (or \fBneomutt\fR)to compose and send the message or not, as +described above. +.TP +.B NMUDIFF_NEWREPORT +This option controls whether a new bug report is made, or whether the +diff is sent to the bugs closed by this NMU. Can be \fImaybe\fR +(default), which sends to the existing bug reports if exactly one bug +is being closed; \fIyes\fR, which always creates a new report, or \fIno\fR, +which always sends to the reports of the bugs being closed (unless no +bugs are being closed, in which case a new report is always made). +.TP +.B BTS_SENDMAIL_COMMAND +If this is set, specifies a \fBsendmail\fR command to use instead of +\fI/usr/sbin/sendmail\fR. Same as the \fB\-\-sendmail\fR command line option. +.SH "SEE ALSO" +.BR debdiff (1), +.BR sensible-editor (1), +.BR devscripts.conf (5) +.SH AUTHOR +\fBnmudiff\fR was written and is copyright 2006 by Steinar +H. Gunderson and modified 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. diff --git a/scripts/nmudiff.sh b/scripts/nmudiff.sh new file mode 100755 index 0000000..7b53777 --- /dev/null +++ b/scripts/nmudiff.sh @@ -0,0 +1,464 @@ +#!/bin/bash +# Copyright 2006 by Steinar H. Gunderson +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 (only) of the GNU General Public License +# as published by the Free Software Foundation. +# +# 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 + Generate a diff for an NMU and mail it to the BTS. + $PROGNAME options: + --new Submit a new bug report rather than sending messages + to the fixed bugs (default if more than one bug being + closed or no bugs being closed) + --old Send reports to the bugs which are being closed rather + than submit a new bug (default if only one bug being + closed) + --sendmail=SENDMAILCMD + Use SENDMAILCMD instead of \"/usr/sbin/sendmail -t\" + --mutt Use mutt to mail the message (default) + --no-mutt Mail the message directly, don't use mutt + --from=EMAIL Use EMAIL address for message to BTS; defaults to + value of DEBEMAIL or EMAIL + --delay=DELAY Indicate that the NMU has been uploaded to the + DELAYED queue, with a delay of DELAY days; defaults + to a placeholder value of "XX". If DELAY is 0 then + no reference is made to the DELAYED queue. + --no-delay Equivalent to \"--delay=0\" + --no-conf, --noconf + Don't read devscripts config files; + must be the first option given + --no-pending, --nopending + Don't add the 'pending' tag + --non-dd, --nondd + Mention in the email that you require sponsorship. + --template=TEMPLATEFILE + Use content of TEMPLATEFILE for message. + --help, -h Show this help information. + --version Show version and copyright information. + +$MODIFIED_CONF_MSG" +} + +version() { + cat <<EOF +This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This code is copyright 2006 by Steinar H. Gunderson, with modifications +copyright 2006 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. +EOF +} + +# Boilerplate: set config variables +DEFAULT_NMUDIFF_DELAY="XX" +DEFAULT_NMUDIFF_MUTT="yes" +DEFAULT_NMUDIFF_NEWREPORT="maybe" +DEFAULT_BTS_SENDMAIL_COMMAND="/usr/sbin/sendmail" +DEFAULT_NMUDIFF_PENDING=" pending" +DEFAULT_MUTT_PRG="mutt" +VARS="NMUDIFF_DELAY NMUDIFF_MUTT NMUDIFF_NEWREPORT BTS_SENDMAIL_COMMAND NMUDIFF_PENDING MUTT_PRG" +# Don't think it's worth including this stuff +# DEFAULT_DEVSCRIPTS_CHECK_DIRNAME_LEVEL=1 +# DEFAULT_DEVSCRIPTS_CHECK_DIRNAME_REGEX='PACKAGE(-.+)?' +# VARS="BTS_SENDMAIL_COMMAND 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 '^(NMUDIFF|BTS|DEVSCRIPTS)_') + + # check sanity + case "$BTS_SENDMAIL_COMMAND" in + "") BTS_SENDMAIL_COMMAND=/usr/sbin/sendmail ;; + *) ;; + esac + if [ "$NMUDIFF_DELAY" = "XX" ]; then + # Fine + : + else + if ! [ "$NMUDIFF_DELAY" -ge 0 ] 2>/dev/null; then + NMUDIFF_DELAY=XX + fi + fi + case "$NMUDIFF_MUTT" in + yes|no) ;; + *) NMUDIFF_MUTT=yes ;; + esac + case "$NMUDIFF_NEWREPORT" in + yes|no|maybe) ;; + *) NMUDIFF_NEWREPORT=maybe ;; + 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 +# Removed: --long check-dirname-level:,check-dirname-regex: \ +TEMP=$(getopt -s bash -o "h" \ + --long sendmail:,from:,new,old,mutt,no-mutt,nomutt \ + --long delay:,no-delay,nodelay \ + --long no-conf,noconf \ + --long no-pending,nopending \ + --long non-dd,nondd \ + --long template: \ + --long help,version -n "$PROGNAME" -- "$@") || (usage >&2; exit 1) + +eval set -- $TEMP + +# Process Parameters +while [ "$1" ]; do + case $1 in +# --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) +# shift; CHECK_DIRNAME_REGEX="$1" ;; + --delay) + shift + if [ "$1" = "XX" ]; then + # Fine + NMUDIFF_DELAY="$1" + else + if ! [ "$1" -ge 0 ] 2>/dev/null; then + NMUDIFF_DELAY=XX + else + NMUDIFF_DELAY="$1" + fi + fi + ;; + --nodelay|--no-delay) + NMUDIFF_DELAY=0 ;; + --nopending|--no-pending) + NMUDIFF_PENDING="" ;; + --nondd|--non-dd) + NMUDIFF_NONDD=yes ;; + --mutt) + NMUDIFF_MUTT=yes ;; + --nomutt|--no-mutt) + NMUDIFF_MUTT=no ;; + --new) + NMUDIFF_NEWREPORT=yes ;; + --old) + NMUDIFF_NEWREPORT=no ;; + --sendmail) + shift + case "$1" in + "") echo "$PROGNAME: SENDMAIL command cannot be empty, using default" >&2 + ;; + *) BTS_SENDMAIL_COMMAND="$1" ;; + esac + ;; + --from) + shift + FROM="$1" + ;; + --no-conf|--noconf) + echo "$PROGNAME: $1 is only acceptable as the first command-line option!" >&2 + exit 1 ;; + --template) + shift + case "$1" in + "") echo "$PROGNAME: TEMPLATEFILE cannot be empty, using default" >&2 + ;; + *) if [ -f "$1" ]; then + NMUDIFF_TEMPLATE="$1" + else + echo "$PROGNAME: TEMPLATEFILE must exist, using default" >&2 + fi + ;; + esac + ;; + --help|-h) 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 + +if [ "$NMUDIFF_MUTT" = yes ]; then + if command -v mutt > /dev/null; then + MUTT_PRG=mutt + elif command -v neomutt > /dev/null; then + MUTT_PRG=neomutt + else + echo "$PROGNAME: can't find mutt, falling back to sendmail instead" >&2 + NMUDIFF_MUTT=no + fi +fi + +if [ "$NMUDIFF_MUTT" = no ]; then + if [ -z "$FROM" ]; then + : ${FROMNAME:="$DEBFULLNAME"} + : ${FROMNAME:="$NAME"} + fi + : ${FROM:="$DEBEMAIL"} + : ${FROM:="$EMAIL"} + if [ -z "$FROM" ]; then + echo "$PROGNAME: must set email address either with DEBEMAIL environment variable" >&2 + echo "or EMAIL environment variable or using --from command line option." >&2 + exit 1 + fi + if [ -n "$FROMNAME" ]; then + # If $FROM looks like "Name <email@address>" then extract just the address + if [ "$FROM" = "$(echo "$FROM" | sed -ne '/^\(.*\) *<\(.*\)> *$/p')" ]; then + FROM="$(echo "$FROM" | sed -ne 's/^\(.*\) *<\(.*\)> *$/\2/p')" + fi + FROM="$FROMNAME <$FROM>" + fi +fi + +if ! [ -f debian/changelog ]; then + echo "nmudiff: must be run from top of NMU build tree!" >&2 + exit 1 +fi + +SOURCE=$(dpkg-parsechangelog -SSource) +if [ -z "$SOURCE" ]; then + echo "nmudiff: could not determine source package name from changelog!" >&2 + exit 1 +fi + +VERSION=$(dpkg-parsechangelog -SVersion) +if [ -z "$VERSION" ]; then + echo "nmudiff: could not determine source package version from changelog!" >&2 + exit 1 +fi + +CLOSES=$(dpkg-parsechangelog -SCloses) + +if [ -z "$CLOSES" ]; then + # no bug reports, so make a new report in any event + NMUDIFF_NEWREPORT=yes +fi + +if [ "$NMUDIFF_NEWREPORT" = "maybe" ]; then + if $(expr match "$CLOSES" ".* " > /dev/null); then + # multiple bug reports, so make a new report + NMUDIFF_NEWREPORT=yes + else + NMUDIFF_NEWREPORT=no + fi +fi + +OLDVERSION=$(dpkg-parsechangelog -o1 -c1 -SVersion) +if [ -z "$OLDVERSION" ]; then + echo "nmudiff: could not determine previous package version from changelog!" >&2 + exit 1 +fi + +VERSION_NO_EPOCH=$(echo "$VERSION" | sed "s/^[0-9]\+://") +OLDVERSION_NO_EPOCH=$(echo "$OLDVERSION" | sed "s/^[0-9]\+://") + +if [ ! -r ../${SOURCE}_${OLDVERSION_NO_EPOCH}.dsc ]; then + echo "nmudiff: could not read ../${SOURCE}_${OLDVERSION_NO_EPOCH}.dsc" >&2 + exit 1 +fi +if [ ! -r ../${SOURCE}_${VERSION_NO_EPOCH}.dsc ]; then + echo "nmudiff: could not read ../${SOURCE}_${VERSION_NO_EPOCH}.dsc" >&2 + exit 1 +fi + +ret=0 +debdiff ../${SOURCE}_${OLDVERSION_NO_EPOCH}.dsc \ + ../${SOURCE}_${VERSION_NO_EPOCH}.dsc \ + > ../${SOURCE}-${VERSION_NO_EPOCH}-nmu.diff || ret=$? +if [ $ret -ne 0 ] && [ $ret -ne 1 ]; then + echo "nmudiff: debdiff failed, aborting." >&2 + rm -f ../${SOURCE}-${VERSION_NO_EPOCH}-nmu.diff + exit 1 +fi + +TO_ADDRESSES_SENDMAIL="" +TO_ADDRESSES_MUTT="" +BCC_ADDRESS_SENDMAIL="" +BCC_ADDRESS_MUTT="" +TAGS="" +if [ "$NMUDIFF_NEWREPORT" = "yes" ]; then + TO_ADDRESSES_SENDMAIL="submit@bugs.debian.org" + TO_ADDRESSES_MUTT="submit@bugs.debian.org" + TAGS="Package: $SOURCE +Version: $OLDVERSION +Severity: normal +Tags: patch ${NMUDIFF_PENDING}" +else + for b in $CLOSES; do + TO_ADDRESSES_SENDMAIL="$TO_ADDRESSES_SENDMAIL, + $b@bugs.debian.org" + TO_ADDRESSES_MUTT="$TO_ADDRESSES_MUTT $b@bugs.debian.org" + if [ "$(bts select bugs:$b tag:patch)" != "$b" ]; then + TAGS="$TAGS +Control: tags $b + patch" + fi + if [ "$NMUDIFF_DELAY" != "0" ] && [ "$(bts select bugs:$b tag:pending)" != "$b" ] && [ $NMUDIFF_PENDING ]; then + TAGS="$TAGS +Control: tags $b + pending" + fi + done + TO_ADDRESSES_SENDMAIL=$(echo "$TO_ADDRESSES_SENDMAIL" | tail -n +2) + if [ "$TAGS" != "" ]; then + TAGS=$(echo "$TAGS" | tail -n +2) + fi +fi + +TMPNAM="$(mktemp -t "$(basename "$1").XXXXXXXXX")" + +if [ "$NMUDIFF_DELAY" = "XX" ] && [ "$NMUDIFF_TEMPLATE" = "" ] && [ "$NMUDIFF_NONDD" != "yes" ]; then + DELAY_HEADER=" +[Replace XX with correct value]" +fi + +if [ "$NMUDIFF_TEMPLATE" != "" ]; then + BODY=$(cat "$NMUDIFF_TEMPLATE") +elif [ "$NMUDIFF_NONDD" = "yes" ]; then + BODY="$(printf "%s\n\n%s\n%s\n\n%s\n\n%s" \ +"Dear maintainer," \ +"I've prepared an NMU for $SOURCE (versioned as $VERSION). The diff" \ +"is attached to this message." \ +"I require a sponsor to have it uploaded." \ +"Regards.")" +elif [ "$NMUDIFF_DELAY" = "0" ]; then + BODY="$(printf "%s\n\n%s\n%s\n\n%s" \ +"Dear maintainer," \ +"I've prepared an NMU for $SOURCE (versioned as $VERSION). The diff" \ +"is attached to this message." \ +"Regards.")" +else + BODY="$(printf "%s\n\n%s\n%s\n%s\n\n%s" \ +"Dear maintainer," \ +"I've prepared an NMU for $SOURCE (versioned as $VERSION) and" \ +"uploaded it to DELAYED/$NMUDIFF_DELAY. Please feel free to tell me if I" \ +"should delay it longer." \ +"Regards.")" +fi + +if [ "$NMUDIFF_MUTT" = no ]; then + cat <<EOF > "$TMPNAM" +From: $FROM +To: $TO_ADDRESSES_SENDMAIL +Cc: +Bcc: $BCC_ADDRESS_SENDMAIL +Subject: $SOURCE: diff for NMU version $VERSION +Date: $(date -R) +X-NMUDIFF-Version: ###VERSION### + +$TAGS +$DELAY_HEADER + +$BODY + +EOF + + cat ../${SOURCE}-${VERSION_NO_EPOCH}-nmu.diff >> "$TMPNAM" + sensible-editor "$TMPNAM" + if [ $? -ne 0 ]; then + echo "nmudiff: sensible-editor exited with error, aborting." >&2 + rm -f ../${SOURCE}-${VERSION_NO_EPOCH}-nmu.diff "$TMPNAM" + exit 1 + fi + + while : ; do + echo -n "Do you want to go ahead and submit the bug report now? (y/n) " + read response + case "$response" in + y*) break;; + n*) echo "OK, then, aborting." >&2 + rm -f ../${SOURCE}-${VERSION_NO_EPOCH}-nmu.diff "$TMPNAM" + exit 1 + ;; + esac + done + + case "$BTS_SENDMAIL_COMMAND" in + /usr/sbin/sendmail*|/usr/sbin/exim*) + BTS_SENDMAIL_COMMAND="$BTS_SENDMAIL_COMMAND -t" ;; + *) ;; + esac + + $BTS_SENDMAIL_COMMAND < "$TMPNAM" + +else # NMUDIFF_MUTT=yes + cat <<EOF > "$TMPNAM" +$TAGS +$DELAY_HEADER + +$BODY + +EOF + + $MUTT_PRG -s "$SOURCE: diff for NMU version $VERSION" -i "$TMPNAM" \ + -e "my_hdr X-NMUDIFF-Version: ###VERSION###" \ + -a ../${SOURCE}-${VERSION_NO_EPOCH}-nmu.diff $BCC_ADDRESS_MUTT \ + -- $TO_ADDRESSES_MUTT + +fi + +rm -f ../${SOURCE}-${VERSION_NO_EPOCH}-nmu.diff "$TMPNAM" diff --git a/scripts/origtargz.pl b/scripts/origtargz.pl new file mode 100755 index 0000000..c3152bb --- /dev/null +++ b/scripts/origtargz.pl @@ -0,0 +1,438 @@ +#!/usr/bin/perl +# +# origtargz: fetch the orig tarball of a Debian package from various sources, +# and unpack it +# Copyright (C) 2012-2019 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, see <https://www.gnu.org/licenses/>. + +=head1 NAME + +origtargz - fetch the orig tarball of a Debian package from various sources, and unpack it + +=head1 SYNOPSIS + +=over + +=item B<origtargz> [I<OPTIONS>] [B<--unpack>[=B<no>|B<once>|B<yes>]] + +=item B<origtargz> B<--help> + +=back + +=head1 DESCRIPTION + +B<origtargz> downloads the orig tarball of a Debian package, and also unpacks +it into the current directory, if it just contains a F<debian> directory. The +main use for B<origtargz> is with debian-dir-only repository checkouts, but it +is useful as a general tarball download wrapper. The version number for the +tarball to be downloaded is determined from F<debian/changelog>. It should be +invoked from the top level directory of an unpacked Debian source package. + +Various download locations are tried: + +=over 4 + +=item * + +First, an existing file is looked for. + +=item * + +Directories given with B<--path> are searched. + +=item * + +B<pristine-tar> is tried. + +=item * + +B<pristine-lfs> is tried. + +=item * + +B<apt-get source> is tried when B<apt-cache showsrc> reports a matching version. + +=item * + +Finally, B<uscan --download --download-current-version> is tried. + +=back + +When asked to unpack the orig tarball, B<origtargz> will remove all files and +directories from the current directory, except the debian directory, and the +VCS repository directories. I<Note that this will drop all non-committed changes> +for the patch system in use (e.g. source format "3.0 (quilt)"), and will even +remove all patches from the package when no patch system is in use (the +original "1.0" source format). Some VCS control files outside F<debian/> +preserved (F<.bzr-builddeb>, F<.bzr-ignore>, F<.gitignore>, F<.hgignore>), if +stored in VCS. + +The default behavior is to unpack the orig tarball if the current directory +is empty except for a F<debian> directory and the VCS files mentioned above. + +=head1 NOTES + +Despite B<origtargz> being called "targz", it will work with any compression +scheme used for the tarball. + +A similar tool to unpack orig tarballs is B<uupdate>(1). B<uupdate> creates a +new working directory, unpacks the tarball, and applies the Debian F<.diff.gz> +changes. In contrast, B<origtargz> uses the current directory, keeping VCS +metadata. + +For Debian package repositories that keep the full upstream source, other tools +should be used to upgrade the repository from the new tarball. See +B<gbp-import-orig>(1) and B<svn-upgrade>(1) for examples. B<origtargz> is still +useful for downloading the current tarball. + +=head1 OPTIONS + +=over + +=item B<-p>, B<--path> I<directory> + +Add I<directory> to the list of locations to search for an existing tarball. +When found, a hardlink is created if possible, otherwise a symlink. + +=item B<-u>, B<--unpack>[=B<no>|B<once>|B<yes>] + +Unpack the downloaded orig tarball to the current directory, replacing +everything except the debian directory. Existing files are removed, except for +F<debian/> and VCS files. Preserved are: F<.bzr>, F<.bzrignore>, +F<.bzr-builddeb>, F<.git>, F<.gitignore>, F<.hg>, F<.hgignore>, F<_darcs> and +F<.svn>. + +=over + +=item B<no> + +Do not unpack the orig tarball. + +=item B<once> (default when B<--unpack> is not used) + +If the current directory contains only a F<debian> directory (and possibly some +dotfiles), unpack the orig tarball. This is the default behavior. + +=item B<yes> (default for B<--unpack> without argument) + +Always unpack the orig tarball. + +=back + +=item B<-d>, B<--download-only> + +Alias for B<--unpack=no>. + +=item B<-t>, B<--tar-only> + +When using B<apt-get source>, pass B<--tar-only> to it. The default is to +download the full source package including F<.dsc> and F<.diff.gz> or +F<.debian.tar.gz> components so B<debdiff> can be used to diff the last upload +to the next one. With B<--tar-only>, only download the F<.orig.tar.*> file. + +=item B<--clean> + +Remove existing files as with B<--unpack>. Note that like B<--unpack>, this +will remove upstream files even if they are stored in VCS. + +=back + +=cut + +#=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: + +=head1 SEE ALSO + +B<debcheckout>(1), B<gbp-import-orig>(1), B<pristine-tar>(1), B<svn-upgrade>(1), B<uupdate>(1) + +=head1 AUTHOR + +B<origtargz> and this manpage have been written by Christoph Berg +<I<myon@debian.org>>. + +=cut + +# option parsing + +use strict; +use warnings; +use File::Temp qw/tempdir/; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use Pod::Usage; + +my @dirs = (); +my $tar_only = 0; +my $unpack = 'once'; # default when --unpack is not used +my $clean = 0; + +GetOptions( + "path|p=s" => \@dirs, + "download-only|d" => sub { $unpack = 'no' }, + "help|h" => sub { pod2usage({ -exitval => 0, -verbose => 1 }); }, + "tar-only|t" => \$tar_only, + "unpack|u:s" => \$unpack, + "clean" => \$clean, +) or pod2usage({ -exitval => 3 }); + +$unpack = 'yes' + if (defined $unpack and $unpack eq '') + ; # default for --unpack without argument +pod2usage({ -exitval => 3 }) if (@ARGV > 0 or $unpack !~ /^(no|once|yes)$/); + +# get package name and version number + +my ($package, $version, $origversion, $fileversion); + +chdir ".." if (!-f "debian/changelog" and -f "../debian/changelog"); +open F, "debian/changelog" or die "debian/changelog: $!\n"; +my $line = <F>; +close F; +unless ($line =~ /^(\S+) \((\S+)\)/) { + die "could not parse debian/changelog:1: $line"; +} +($package, $version) = ($1, $2); +unless ($version =~ /-/) { + print +"Package with native version number $version, skipping orig.tar.* download\n"; + exit 0; +} +$origversion = $version; +$origversion =~ s/(.*)-.*/$1/; # strip everything from the last dash +$fileversion = $origversion; +$fileversion =~ s/^\d+://; # strip epoch + +# functions + +sub download_origtar () { + # look for an existing file + + if (my @f = glob "../${package}_$fileversion.orig*.tar.*") { + print "Using existing $f[0]\n"; + return @f; + } + + # try other directories + + foreach my $dir (@dirs) { + $dir =~ s!/$!!; + + if (my @f = glob "$dir/${package}_$fileversion.orig*.tar.*") { + my @res; + for my $f (@f) { + print "Using $f\n"; + my $basename = $f; + $basename =~ s!.*/!!; + link $f, "../$basename" + or symlink $f, "../$basename" + or die "symlink: $!"; + push @res, "../$basename"; + } + return @res; + } + } + + # try pristine-tar + + my @files + = grep { /^\Q${package}_$fileversion.orig\E(?:-[\w\-]+)?\.tar\./ } + map { chomp; $_; } # remove newlines + `pristine-tar list 2>&1`; + if (@files) { + system "pristine-tar checkout ../$_" for @files; + } + + if (my @f = glob "../${package}_$fileversion.orig*.tar.*") { + return @f; + } + + # try pristine-lfs + + @files = grep { /^\Q${package}_$fileversion.orig\E(?:-[\w\-]+)?\.tar\./ } + map { chomp; $_; } # remove newlines + `pristine-lfs list 2>&1`; + if (@files) { + system "pristine-lfs checkout -o .. $_" for @files; + } + + if (my @f = glob "../${package}_$fileversion.orig*.tar.*") { + return @f; + } + + # try apt-get source + + open S, "apt-cache showsrc '$package' |"; + my @showsrc; + { + local $/ = ""; # slurp paragraphs + @showsrc = <S>; + } + close S; + + my $bestsrcversion; + foreach my $src (@showsrc) { + $src =~ /^Package: (.*)/m or next; + next if ($1 ne $package); + ; # should never trigger, but who knows + $src =~ /^Version: (.*)/m or next; + my $srcversion = $1; + my $srcorigversion = $srcversion; + $srcorigversion =~ s/(.*)-.*/$1/; # strip everything from the last dash + + if ($srcorigversion eq $origversion) + { # loop through all matching versions + $bestsrcversion = $srcversion; + last if ($srcversion eq $version); # break if exact match + } + } + + if ($bestsrcversion) { + print "Trying apt-get source $package=$bestsrcversion ...\n"; + my $t = $tar_only ? '--tar-only' : ''; + system +"cd .. && apt-get source --only-source --download-only $t '$package=$bestsrcversion'"; + } + + if (my @f = glob "../${package}_$fileversion.orig*.tar.*") { + return @f; + } + + # try uscan + + if (-f "debian/watch") { + print "Trying uscan --download --download-current-version ...\n"; + system "uscan --download --download-current-version --rename\n"; + } + + if (my @f = glob "../${package}_$fileversion.orig*.tar.*") { + return @f; + } + + print + "Could not find any location for ${package}_$fileversion.orig.tar.*\n"; + return; +} + +sub clean_checkout () { + # delete all files except debian/, our VCS checkout, and some files + # often in VCS outside debian/ even in debian-dir-only repositories + opendir DIR, '.' or die "opendir: $!"; + my @rm; + while (my $file = readdir DIR) { + next if ($file eq '.' or $file eq '..'); + next if ($file eq 'debian'); + next if ($file =~ /^(\.bzr|\.git|\.hg|\.svn|CVS|_darcs)$/); + if ($file eq '.gitignore' and -d '.git') + { # preserve .gitignore if it's from git + next if `git ls-files .gitignore` eq ".gitignore\n"; + } + if ( ($file =~ /^\.bzr(ignore|-builddeb)$/ and -d '.bzr') + or ($file eq '.hgignore' and -d '.hg')) { + print +"Notice: not deleting $file (likely to come from VCS checkout)\n"; + next; + } + push @rm, $file; + } + close DIR; + system('rm', '-rf', '--', @rm); +} + +sub unpack_tarball (@) { + my @origtar = @_; + + for my $origtar (@origtar) { + if ($origtar =~ m/\.asc$/) { + next; + } + + my $tmpdir = File::Temp->newdir(DIR => ".", CLEANUP => 1); + print "Unpacking $origtar\n"; + my $cmp = ($origtar =~ /orig(?:-([\w\-]+))?\.tar/)[0] || ''; + if ($cmp) { + mkdir $cmp; + $cmp = "/$cmp"; + mkdir "$tmpdir$cmp"; + } + #print STDERR Dumper(\@origtar,$cmp);use Data::Dumper;exit; + + # unpack + system('tar', "--directory=$tmpdir$cmp", '-xf', "$origtar"); + if ($? >> 8) { + print STDERR "unpacking $origtar failed\n"; + return 0; + } + + # figure out which subdirectory was created by unpacking + my $directory; + my @files = glob "$tmpdir$cmp/*"; + if (@files == 1 and -d $files[0]) + { # exactly one directory, move its contents over + $directory = $files[0]; + } else + { # several files were created, move these to the target directory + $directory = $tmpdir . $cmp; + } + + # move all files over, except the debian directory + opendir DIR, $directory or die "opendir $directory: $!"; + foreach my $file (readdir DIR) { + if ($file eq 'debian') { + system('rm', '-rf', '--', "$directory/$file"); + next; + } elsif ($file eq '.' or $file eq '..') { + next; + } + my $dest = './' . ($cmp ? "$cmp/" : '') . $file; + unless (rename "$directory/$file", $dest) { + print `ls -l $directory/$file`; + print STDERR "rename $directory/$file $dest: $!\n"; + return 0; + } + } + closedir DIR; + rmdir $directory; + } + + return 1; +} + +# main + +if ($clean) { + clean_checkout; + exit 0; +} + +my @origtar = download_origtar; +exit 1 unless (@origtar); + +if ($unpack eq 'once') { + my @files = glob '*'; # ignores dotfiles + if (@files == 1) + { # this is debian/, we have already opened debian/changelog + unpack_tarball(@origtar) or exit 1; + } +} elsif ($unpack eq 'yes') { + clean_checkout; + unpack_tarball(@origtar) or exit 1; +} + +exit 0; diff --git a/scripts/pkgnames.bash_completion b/scripts/pkgnames.bash_completion new file mode 100644 index 0000000..eef8c41 --- /dev/null +++ b/scripts/pkgnames.bash_completion @@ -0,0 +1,24 @@ +# /usr/share/bash-completion/completions/pkgnames +# Bash command completion for commands that expect a Debian package name. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +_pkg_names() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + + COMPREPLY=( $( apt-cache pkgnames $cur 2> /dev/null ) ) + + return 0 +} + +complete -F _pkg_names wnpp-alert wnpp-check mk-build-deps rmadison mass-bug debsnap dd-list build-rdeps who-uploads transition-check getbuildlog grep-excuses rc-alert whodepends dget pts-subscribe pts-unsubscribe + + +# Local variables: +# coding: utf-8 +# mode: shell-script +# indent-tabs-mode: nil +# End: +# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 : diff --git a/scripts/plotchangelog.1 b/scripts/plotchangelog.1 new file mode 100644 index 0000000..acbe66d --- /dev/null +++ b/scripts/plotchangelog.1 @@ -0,0 +1,127 @@ +.TH PLOTCHANGELOG 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +plotchangelog \- graph Debian changelogs +.SH SYNOPSIS +.B plotchangelog +.I "\fR[\fPoptions\fR]\fP changelog \fR...\fP" +.SH "DESCRIPTION" +.BR plotchangelog +is a tool to aid in visualizing a Debian \fIchangelog\fR. The changelogs are +graphed with +.BR gnuplot (1) +, with the X axis of the graph denoting time of release and the Y axis +denoting the Debian version number of the package. Each individual release +of the package is represented by a point, and the points are color coded to +indicate who released that version of the package. The upstream version +number of the package can also be labeled on the graph. +.PP +Alternatively, the Y axis can be configured to display the size of the +changelog entry for each new version. Or it can be configured to display +approximately how many bugs were fixed for each new version. +.PP +Note that if the package is a Debian-specific package, the entire package +version will be used for the Y axis. This does not always work perfectly. +.PP +.SH "READING THE GRAPH" +The general outline of a package's +graph is typically a series of peaks, starting at 1, going up to n, and then +returning abruptly to 1. The higher the peaks, the more releases the +maintainer made between new upstream versions of the package. If a package +is Debian-only, it's graph will just grow upwards without ever falling +(although a bug in this program may cause it to fall sometimes, if the +version number goes from say, 0.9 to say, 0.10 - this is interpreted wrong...) +.PP +If the graph dips below 1, someone made a NMU of the package and upgraded it +to a new upstream version, thus setting the Debian version to 0. NMU's in +general appear as fractional points like 1.1, 2.1, etc. A NMU can also be +easily detected by looking at the points that represent which maintainer +uploaded the package -- a solitary point of a different type than the points +before and after it is typically a NMU. +.PP +It's also easy to tell by looking at the points when a package changes +maintainers. +.SH OPTIONS +.TP +.B \-l\fR, \fP\-\-linecount +Instead of using the Debian version number as the Y axis, use the number of +lines in the changelog entry for each version. Cannot be used +together with +.BR \-\-bugcount . +.TP +.B \-b\fR, \fP\-\-bugcount +Instead of using the Debian version number as the Y axis, use the number of +bugs that were closed by each changelog entry. Note that this number is +obtained by searching for "#dddd" in the changelog, and so it may be +inaccurate. Cannot be used together with +.BR \-\-linecount . +.TP +.B \-c\fR, \fP\-\-cumulative +When used together with either +.B \-\-bugcount +or +.BR \-\-linecount , +graphs the cumulative count rather than the count in each individual +changelog entry. +.TP +.B \-v\fR, \fP\-\-no-version +Do not show upstream version labels. Useful if the graph gets too crowded. +.TP +.B \-m, \-\-no-maint +Do not differentiate between different maintainers of the package. +.TP +.B \-s file\fR, \fP\-\-save=\fIfile +Save the graph to \fIfile\fR in PostScript format instead of immediately +displaying it. +.TP +.B \-u\fR, \fP\-\-urgency +Use larger points when displaying higher-urgency package uploads. +.TP +.B \-\-verbose +Output the gnuplot script that is fed into gnuplot (for debugging purposes). +.TP +.B \-g\fIcommands\fR, \fB\-\-gnuplot=\fIcommands +This allows you to insert +.BR gnuplot (1) +commands into the gnuplot script that is used to generate the graph. The +commands are placed after all initialization but before the final \fBplot\fR +command. This can be used to override the default look provided by this +program in arbitrary ways. You can also use things like +"set terminal png color" +to change the output file type, which is useful in conjunction with +the \-s option. +.TP +.B \-\-help +Show a usage summary. +.TP +.B \-\-version +Display version, author and copyright information. +.TP +.B \-\-noconf\fR, \fP\-\-no-conf +Do not read any configuration files (see below). +.TP +.I changelog \fR... +The \fIchangelog\fR files to graph. If multiple files are specified they will all +be displayed on the same graph. The files may be compressed with gzip. Any +text in them that is not in Debian changelog format will be ignored. +.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. 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 PLOTCHANGELOG_OPTIONS +This is a space-separated list of options to always use, for example +\fB\-l \-b\fP. Do not include \fB\-g\fP or \fB\-\-gnuplot\fP among this list as it +may be ignored; see the next variable instead. +.TP +.B PLOTCHANGELOG_GNUPLOT +These are +.B gnuplot +commands which will be prepended to any such commands given on the +command line. +.SH "SEE ALSO" +.BR devscripts.conf (5) +.SH AUTHOR +Joey Hess <joey@kitenet.net> diff --git a/scripts/plotchangelog.bash_completion b/scripts/plotchangelog.bash_completion new file mode 100644 index 0000000..6b81bd6 --- /dev/null +++ b/scripts/plotchangelog.bash_completion @@ -0,0 +1,33 @@ +# /usr/share/bash-completion/completions/plotchangelog +# Bash command completion for ‘plotchangelog(1)’. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +# Copyright © 2015, Nicholas Bamber <nicholas@periapt.co.uk> + +_plotchangelog() +{ + local cur prev words cword _options + _init_completion || return + + _options='--linecount --bugcount --cumulative --no-version --no-maint --urgency --verbose' + + if [[ $prev == plotchangelog ]]; then + _options+=' --no-conf' + fi + + _options+=' ' + _options+=$(find . -name changelog | sed -e's!\.\/!!' | paste -s -d' ') + + COMPREPLY=( $( compgen -W "${_options}" -- "$cur" ) ) + + return 0 +} && +complete -F _plotchangelog plotchangelog + + +# Local variables: +# coding: utf-8 +# mode: shell-script +# indent-tabs-mode: nil +# End: +# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 : diff --git a/scripts/plotchangelog.pl b/scripts/plotchangelog.pl new file mode 100755 index 0000000..3813ad6 --- /dev/null +++ b/scripts/plotchangelog.pl @@ -0,0 +1,429 @@ +#!/usr/bin/perl +# +# Plot the history of a debian package from the changelog, displaying +# when each release of the package occurred, and who made each release. +# To make the graph a little more interesting, the debian revision of the +# package is used as the y axis. +# +# Pass this program the changelog(s) you wish to be plotted. +# +# Copyright 1999 by Joey Hess <joey@kitenet.net> +# Modifications copyright 2003 by 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.006; +use strict; +use FileHandle; +use File::Basename; +use File::Temp qw/ tempfile /; +use Fcntl; +use Getopt::Long qw(:config bundling permute no_getopt_compat); + +BEGIN { + pop @INC if $INC[-1] eq '.'; + eval { require Date::Parse; import Date::Parse(); }; + if ($@) { + my $progname = basename($0); + if ($@ =~ /^Can\'t locate Date\/Parse\.pm/) { + die +"$progname: you must have the libtimedate-perl package installed\nto use this script\n"; + } else { + die +"$progname: problem loading the Date::Parse module:\n $@\nHave you installed the libtimedate-perl package?\n"; + } + } +} + +my $progname = basename($0); +my $modified_conf_msg; + +sub usage { + print <<"EOF"; +Usage: plotchangelog [options] changelog ... + -v --no-version Do not show package version information. + -m --no-maint Do not show package maintainer information. + -u --urgency Use larger points for higher urgency uploads. + -l --linecount Make the Y axis be number of lines in the + changelog. + -b --bugcount Make the Y axis be number of bugs closed + in the changelog. + -c --cumulative With -l or -b, graph the cumulative number + of lines or bugs closed. + -g "commands" Pass "commands" on to gnuplot, they will be + --gnuplot="commands" added to the gnuplot script that is used to + generate the graph. + -s file --save=file Save the graph to the specified file in + postscript format. + -d --dump Dump gnuplot script to stdout. + --verbose Outputs the gnuplot script. + --help Show this message. + --version Display version and copyright information. + --noconf --no-conf Don\'t read devscripts configuration files + Must be the first option. + + At most one of -l and -b (or their long equivalents) may be used. + +Default settings modified by devscripts configuration files: +$modified_conf_msg +EOF +} + +my $versioninfo = <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +This code is copyright 1999 by Joey Hess <joey\@kitenet.net>. +Modifications copyright 1999-2003 by 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 + +my ( + $no_version, $no_maintainer, $gnuplot_commands, $dump, + $save_filename, $verbose, $linecount, $bugcount, + $cumulative, $help, $showversion, $show_urgency, + $noconf +) = ""; + +# Handle config file unless --no-conf or --noconf is specified +# The next stuff is boilerplate +my $extra_gnuplot_commands = ''; +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 = ( + 'PLOTCHANGELOG_OPTIONS' => '', + 'PLOTCHANGELOG_GNUPLOT' => '', + ); + 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; + + if ($config_vars{'PLOTCHANGELOG_OPTIONS'}) { + unshift @ARGV, split(' ', $config_vars{'PLOTCHANGELOG_OPTIONS'}); + } + $extra_gnuplot_commands = $config_vars{'PLOTCHANGELOG_GNUPLOT'}; +} + +GetOptions( + "no-version|v", \$no_version, + "no-maint|m", \$no_maintainer, + "gnuplot|g=s", \$gnuplot_commands, + "save|s=s", \$save_filename, + "dump|d", \$dump, + "urgency|u", \$show_urgency, + "verbose", \$verbose, + "l|linecount", \$linecount, + "b|bugcount", \$bugcount, + "c|cumulative", \$cumulative, + "help", \$help, + "version", \$showversion, + "noconf" => \$noconf, + "no-conf" => \$noconf, + ) + or die +"Usage: $progname [options] changelog ...\nRun $progname --help for more details\n"; + +if ($noconf) { + die +"$progname: --no-conf is only acceptable as the first command-line option!\n"; +} + +if ($help) { + usage(); + exit 0; +} + +if ($showversion) { + print $versioninfo; + exit 0; +} + +if ($bugcount && $linecount) { + die +"$progname: can't use --bugcount and --linecount\nRun $progname --help for usage information.\n"; +} + +if ($cumulative && !$bugcount && !$linecount) { + warn +"$progname: --cumulative without --bugcount or --linecount: ignoring\nRun $progname --help for usage information.\n"; +} + +if (!@ARGV) { + die +"Usage: $progname [options] changelog ...\nRun $progname --help for more details\n"; +} + +my %data; +my ($package, $version, $maintainer, $date, $urgency) = undef; +my ($data_tmpfile, $script_tmpfile); +my ($data_fh, $script_fh); + +if (!$dump) { + $data_fh = tempfile("plotdataXXXXXX", UNLINK => 1) + or die "cannot create temporary file: $!"; + fcntl $data_fh, Fcntl::F_SETFD(), 0 + or die "disabling close-on-exec for temporary file: $!"; + $script_fh = tempfile("plotscriptXXXXXX", UNLINK => 1) + or die "cannot create temporary file: $!"; + fcntl $script_fh, Fcntl::F_SETFD(), 0 + or die "disabling close-on-exec for temporary file: $!"; + $data_tmpfile = '/dev/fd/' . fileno($data_fh); + $script_tmpfile = '/dev/fd/' . fileno($script_fh); +} else { + $data_tmpfile = '-'; +} +my %pkgcount; +my $c; + +# Changelog parsing. +foreach (@ARGV) { + if (/\.gz$/) { + open F, "zcat $_|" || die "$_: $!"; + } else { + open F, $_ || die "$_: $!"; + } + + while (<F>) { + chomp; + # Note that some really old changelogs use priority, not urgency. + if (/^(\w+.*?)\s+\((.*?)\)\s+.*?;\s+(?:urgency|priority)=(.*)/i) { + $package = lc($1); + $version = $2; + if ($show_urgency) { + $urgency = $3; + if ($urgency =~ /high/i) { + $urgency = 2; + } elsif ($urgency =~ /medium/i) { + $urgency = 1.5; + } else { + $urgency = 1; + } + } else { + $urgency = 1; + } + undef $maintainer; + undef $date; + $c = 0; + } elsif (/^ -- (.*?) (.*)/) { + $maintainer = $1; + $date = str2time($2); + + # Strip email address. + $maintainer =~ s/<.*>//; + $maintainer =~ s/\(.*\)//; + $maintainer =~ s/\s+$//; + } elsif (/^(\w+.*?)\s+\((.*?)\)\s+/) { + print STDERR qq[Parse error on "$_"\n]; + } elsif ($linecount && /^ /) { + $c++; # count changelog size. + } elsif ($bugcount && /^ /) { + # count bugs that were said to be closed. + my @bugs = m/#\d+/g; + $c += $#bugs + 1; + } + + if ( defined $package + && defined $version + && defined $maintainer + && defined $date + && defined $urgency) { + $data{$package}{ $pkgcount{$package}++ } = [ + $linecount || $bugcount ? $c : $version, + $maintainer, $date, $urgency + ]; + undef $package; + undef $version; + undef $maintainer; + undef $date; + undef $urgency; + } + } + + close F; +} + +if ($cumulative) { + # have to massage the data; based on some code from later on + foreach $package (keys %data) { + my $total = 0; + # It's crucial the output is sorted by date. + foreach my $i ( + sort { $data{$package}{$a}[2] <=> $data{$package}{$b}[2] } + keys %{ $data{$package} } + ) { + $total += $data{$package}{$i}[0]; + $data{$package}{$i}[0] = $total; + } + } +} + +my $header = q{ +set key below title "key" box +set timefmt "%m/%d/%Y %H:%M" +set xdata time +set format x "%m/%y" +set yrange [0 to *] +}; +if ($linecount) { + if ($cumulative) { + $header .= "set ylabel 'Cumulative changelog length'\n"; + } else { + $header .= "set ylabel 'Changelog length'\n"; + } +} elsif ($bugcount) { + if ($cumulative) { $header .= "set ylabel 'Cumulative bugs closed'\n"; } + else { $header .= "set ylabel 'Bugs closed'\n"; } +} else { + $header .= "set ylabel 'Debian version'\n"; +} +if ($save_filename) { + $header .= "set terminal postscript color solid\n"; + $header .= "set output '$save_filename'\n"; +} +my $script = "plot "; +my $data = ''; +my $index = 0; +my %maintdata; + +# Note that "lines" is used if we are also showing maintainer info, +# otherwise we use "linespoints" to make sure points show up for each +# release anyway. +my $style = $no_maintainer ? "linespoints" : "lines"; + +foreach $package (keys %data) { + my $oldmaintainer = ""; + my $oldversion = ""; + # It's crucial the output is sorted by date. + foreach my $i ( + sort { $data{$package}{$a}[2] <=> $data{$package}{$b}[2] } + keys %{ $data{$package} } + ) { + my $v = $data{$package}{$i}[0]; + $maintainer = $data{$package}{$i}[1]; + $date = $data{$package}{$i}[2]; + $urgency = $data{$package}{$i}[3]; + + $maintainer =~ s/"/\\"/g; + + my $y; + + # If it's got a debian revision, use that as the y coordinate. + if ($v =~ m/(.*)-(.*)/) { + $y = $2; + $version = $1; + } else { + $y = $v; + } + + # Now make sure the version is a real number. This includes making + # sure it has no more than one decimal point in it, and getting rid of + # any nonnumeric stuff. Otherwise, the "set label" command below could + # fail. Luckily, perl's string -> num conversion is perfect for this job. + $y = $y + 0; + + if (lc($maintainer) ne lc($oldmaintainer)) { + $oldmaintainer = $maintainer; + } + + my ($sec, $min, $hour, $mday, $mon, $year) = localtime($date); + my $x = ($mon + 1) . "/$mday/" . (1900 + $year) . " $hour:$min"; + $data .= "$x\t$y\n"; + $maintdata{$oldmaintainer}{$urgency} .= "$x\t$y\n"; + + if ($oldversion ne $version && !$no_version) { + # Upstream version change. Label it. + $header .= "set label '$version' at '$x',$y left\n"; + $oldversion = $version; + } + } + $data .= "\n\n"; # start new dataset + # Add to plot command. + $script + .= "'$data_tmpfile' index $index using 1:3 title '$package' with $style, "; + $index++; +} + +# Add a title. +my $title .= "set title '"; +$title + .= $#ARGV > 1 + ? "Graphing Debian changelogs" + : "Graphing Debian changelog"; +$title .= "'\n"; + +if (!$no_maintainer) { + foreach $maintainer (sort keys %maintdata) { + foreach $urgency (sort keys %{ $maintdata{$maintainer} }) { + $data .= $maintdata{$maintainer}{$urgency} . "\n\n"; + $script + .= "'$data_tmpfile' index $index using 1:3 title \"$maintainer\" with points pointsize " + . (1.5 * $urgency) . ", "; + $index++; + } + } +} + +$script =~ s/, $/\n/; +$script = qq{ +$header +$title +$extra_gnuplot_commands +$gnuplot_commands +$script +}; +$script .= "pause -1 'Press Return to continue.'\n" + unless $save_filename || $dump; + +if (!$dump) { + # Annoyingly, we have to use 2 temp files. I could just send everything to + # gnuplot on stdin, but then the pause -1 doesn't work. + open(DATA, ">$data_tmpfile") || die "$data_tmpfile: $!"; + open(SCRIPT, ">$script_tmpfile") || die "$script_tmpfile: $!"; +} else { + open(DATA, ">&STDOUT"); + open(SCRIPT, ">&STDOUT"); +} + +print SCRIPT $script; +print $script if $verbose && !$dump; +print DATA $data; +close SCRIPT; +close DATA; + +if (!$dump) { + unless (system("gnuplot", $script_tmpfile) == 0) { + die "gnuplot program failed (is the gnuplot package installed?): $!\n"; + } +} diff --git a/scripts/pts-subscribe.1 b/scripts/pts-subscribe.1 new file mode 100644 index 0000000..bb1d459 --- /dev/null +++ b/scripts/pts-subscribe.1 @@ -0,0 +1,59 @@ +.TH PTS-SUBSCRIBE 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +pts-subscribe \- time-limited subscription to the PTS +.SH SYNOPSIS +\fBpts-subscribe\fR [\fIoptions\fR] \fIpackage\fR +.PP +\fBpts-unsubscribe\fR [\fIoptions\fR] \fIpackage\fR +.SH DESCRIPTION +\fBpts-subscribe\fR sends a subscription request for \fIpackage\fR to +the Package Tracking System at pts@qa.debian.org, and cancels the +subscription 30 days later. +.PP +If called as \fBpts-unsubscribe\fR, send an unsubscription request +for \fIpackage\fR to the Package Tracking System. +.PP +This utility is useful if a developer has made an NMU and wants to +track the package for a limited period of time. +.SH OPTIONS +.TP +\fB\-\-until \fItime\fR, \fB\-u\fR \fItime\fR +When \fBat\fR(1) should cancel the subscription. \fItime\fR must be +specified using \fBat\fR's syntax. Default is 'now + 30 days'. This +option will probably require quoting! +.TP +.B \-\-forever +Don't cancel the subscription automatically. This can also be +specified as \fB\-\-until forever\fR. +.TP +.BR \-\-help ", " \-h +Display a help message and exit successfully. +.TP +.B \-\-version +Display version and copyright information and exit successfully. +.SH "ENVIRONMENT VARIABLES" +.TP +.BR DEBEMAIL ", " EMAIL +If one of these is set (with preference give to \fBDEBEMAIL\fR), then this +will be used for the subscription address. If neither is set, then +the email will be sent without a specified subscription address, and +the email's From: line will be used to determine the sender's +address. This will be determined by \fBmail\fR(1). +.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 PTS_UNTIL +Setting this is equivalent to giving a \fB\-\-until\fR option. +.SH "SEE ALSO" +.BR at (1), +information about the Package Tracking System in the Developer's +Reference at +https://www.debian.org/doc/developers-reference/resources.html#pkg-tracking-system +.SH AUTHOR +This program was written by Julian Gilbey <jdg@debian.org> based on a +public domain prototype by Raphael Hertzog <hertzog@debian.org> and is +copyright under the GPL, version 2 or later. diff --git a/scripts/pts-subscribe.sh b/scripts/pts-subscribe.sh new file mode 100755 index 0000000..b964f4d --- /dev/null +++ b/scripts/pts-subscribe.sh @@ -0,0 +1,176 @@ +#!/bin/bash +set -e + +# Subscribe to the PTS for a specified package for a limited length of time + +PROGNAME=${0##*/} +MODIFIED_CONF_MSG='Default settings modified by devscripts configuration files:' + +usage() { + echo \ +"Usage: $PROGNAME [options] package + Subscribe to the PTS (Package Tracking System) for the specified package + for a limited length of time (30 days by default). + + If called as 'pts-unsubscribe', unsubscribe from the PTS for the specified + package. + + Options: + -u, --until UNTIL + When to unsubscribe; this is given as the command-line + argument to at (default: 'now + 30 days') + + --until 0, --until forever are synonyms for --forever + + --forever Do not set an at job for unsubscribing + + --no-conf, --noconf + Don't read devscripts config files; + must be the first option given + + --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 2006 by Julian Gilbey, all rights reserved. +Original public domain code by Raphael Hertzog. +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." +} + +ACTION="subscribe" +if [ "$PROGNAME" = "pts-unsubscribe" ]; then + ACTION="unsubscribe" +fi + +# Boilerplate: set config variables +DEFAULT_PTS_UNTIL='now + 30 days' +VARS="PTS_UNTIL" + +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 '^PTS_') + + # check sanity - nothing to do here (at will complain if it's illegal) + + # 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 + +# Will bomb out if there are unrecognised options +TEMP=$(getopt -s bash -o "u:" \ + --long until:,forever \ + --long no-conf,noconf \ + --long help,version -n "$PROGNAME" -- "$@") || (usage >&2; exit 1) + +eval set -- $TEMP + +# Process Parameters +while [ "$1" ]; do + case $1 in + --until|-u) + shift + PTS_UNTIL="$1" + ;; + --forever) + PTS_UNTIL="forever" ;; + --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 [ $# -ne 1 ]; then + echo "$PROGNAME takes precisely one non-option argument: the package name;" >&2 + echo "try $PROGNAME --help for usage information" >&2 + exit 1 +fi + +# Check for a "mail" command +if ! command -v mail > /dev/null; then + echo "$PROGNAME: Could not find the \"mail\" command; you must have the" >&2 + echo "bsd-mailx or mailutils package installed to run this script." >&2 + exit 1 +fi + +pkg=$1 + +if [ -z "$DEBEMAIL" ]; then + if [ -z "$EMAIL" ]; then + echo "$PROGNAME warning: \$DEBEMAIL is not set; attempting to $ACTION anyway" >&2 + else + echo "$PROGNAME warning: \$DEBEMAIL is not set; using \$EMAIL instead" >&2 + DEBEMAIL=$EMAIL + fi +fi +DEBEMAIL=$(echo $DEBEMAIL | sed -s 's/^.*[ ]<\(.*\)>.*/\1/') + +if [ "$ACTION" = "unsubscribe" ]; then + echo "$ACTION $pkg $DEBEMAIL" | mail pts@qa.debian.org +else + # Check for an "at" command + if [ "$PTS_UNTIL" != forever -a "$PTS_UNTIL" != 0 ]; then + if ! command -v at > /dev/null; then + echo "$PROGNAME: Could not find the \"at\" command; you must have the" >&2 + echo "\"at\" package installed to run this script." >&2 + exit 1 + fi + + cd / + TEMPFILE=$(mktemp --tmpdir pts-subscribe.tmp.XXXXXXXXXX) || { echo "$PROGNAME: Couldn't create tempfile!" >&2; exit 1; } + trap 'rm -f "$TEMPFILE"' EXIT + echo "echo 'unsubscribe $pkg $DEBEMAIL' | mail pts@qa.debian.org" | \ + at $PTS_UNTIL 2>$TEMPFILE + grep '^job ' $TEMPFILE | sed -e 's/^/Unsubscription will be sent by "at" as /' + else + echo "No unsubscription request will be sent" + fi + + echo "$ACTION $pkg $DEBEMAIL" | mail pts@qa.debian.org +fi diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml new file mode 100644 index 0000000..963b009 --- /dev/null +++ b/scripts/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 88 + +[tool.isort] +line_length = 88 +profile = "black" diff --git a/scripts/rc-alert.1 b/scripts/rc-alert.1 new file mode 100644 index 0000000..8250aaa --- /dev/null +++ b/scripts/rc-alert.1 @@ -0,0 +1,129 @@ +.TH RC-ALERT 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +rc-alert \- check for installed packages with release-critical bugs +.SH SYNOPSIS +\fBrc\-alert\fR [\fIinclusion options\fR] [\fB\-\-debtags\fR [\fItag\fR[\fB,\fItag\fR ...]]] [\fB\-\-popcon\fR] [\fIpackage\fR ...] +.br +\fBrc\-alert \-\-help\fR|\fB\-\-version\fR +.SH DESCRIPTION +\fBrc\-alert\fR downloads the list of release-critical bugs from the +Debian BTS webpages, and then outputs a list of packages installed on +the system, or given on the command-line, which are in that list. +.P +If the directory \fI$XDG_CACHE_HOME/devscripts/rc-alert\fP exists or the +\fB\-\-cache\fP option is given, then the (sizable) downloaded list +will be cached, and will only be downloaded again on a second +invocation if it has changed. +.SH OPTIONS +.TP +.BR \-\-cache +Force the creation of the \fI$XDG_CACHE_HOME/devscripts/rc-alert\fP cache directory. +.TP +.BR \-\-help ", " \-h +Show a summary of options. +.TP +.BR \-\-version ", " \-v +Show version and copyright information. +.P +It is also possible to filter the list of bugs reported based on the +tags and distributions associated with the package. The filtering options +are: +.TP +.BR \-\-include\-tags ", " \-f +A list of tags which the bug must have, in the format used for output. +For example, to include bugs tagged security or help wanted, use "SH". +.TP +.BR \-\-include\-tag\-op ", " \-t +If set to \fIand\fP, a bug must have all of the tags specified by +\fB\-\-include\-tags\fP. +.TP +.BR \-\-exclude\-tags +A list of tags which the bug must not have, in the same format as +\fB\-\-include\-tags\fP. +.TP +.BR \-\-exclude\-tag\-op +If set to \fIand\fP, a bug must have none of the tags specified by +\fB\-\-exclude\-tags\fP. By default, the bug will be excluded if any tag +matches. +.TP +.BR \-\-include\-dists ", " \-d +A list of distributions which the bug must apply to, in the format used for +output. For example, to include bugs affecting testing or unstable, use "TU". +.TP +.BR \-\-include\-dist\-op ", " \-o +If set to \fIand\fP, a bug must apply to all of the specified distributions in +order to be included. +.TP +.BR \-\-exclude\-dists +A list of distributions to which the bug must not apply, in the same format as +\fB\-\-include\-dists\fP. +.TP +.BR \-\-exclude\-dist\-op +If set to \fIand\fP, a bug must apply to all of the specified distributions +in order to be excluded. By default the bug will be excluded if it applies +to any of the listed distributions. +.P +It is also possible to only list bugs which have specific debtags set. Note +that you need to have debtags installed and also that it's not mandatory for +maintainers to set proper debtags. The produced list will thus probably be +incomplete. +.TP +.BR \-\-debtags +Match packages based on the listed tags. Each package is matched only if it has +all the listed tags; in the case of multiple tags within the same facet, a +package is matched if it has any of the listed tags within the facet. +.TP +.BR \-\-debtags\-database +Use a non-standard debtags database. The default is +\fI/var/lib/debtags/packages-tags\fP. +.P +Popularity-contest collects data about installation and usage of Debian +packages. You can additionally sort the bugs by the popcon rank of the related +packages. +.TP +.BR \-\-popcon +Sort bugs by the popcon rank of the package the bug belongs to. +.TP +.BR \-\-pc\-vote +By default, packages are sorted according to the number of people who have the +package installed. This option enables sorting by the number of people +regularly using the package instead. This option has no effect in combination +with \-\-pc\-local. +.TP +.BR \-\-pc\-local +Instead of requesting remote data the information from the last popcon run is +used (\fI/var/log/popularity-contest\fP). +.SH EXAMPLES +.TP +.BR \-\-include\-dists " OS" +The bug must apply to at least one of oldstable or stable +.TP +.BR \-\-include\-dists " TU" " \-\-include\-dist\-op" " and" +The bug must apply to both testing and unstable +.TP +.BR \-\-include\-dists " O" " \-\-include\-tags" " S" " \-\-exclude\-tags" " +" +The bug must apply to oldstable and be tagged security but not patch +.TP +.BR \-\-exclude\-dists " SOT" " \-\-include\-tags" " R" +The bug must apply to only unstable or experimental (or both) and be tagged +unreproducible +.TP +.BR \-\-debtags " implemented-in::perl,role::plugin,implemented-in::python" +The bug must apply to packages matching the specified debtags, i.e. the match +will only include packages that have the 'role::plugin' tag and that have +either of the tags 'implemented-in::perl' or 'implemented-in::python'. +.TP +.BR \-\-popcon " "\-\-pc\-local +Read \fI/var/log/popularity-contest\fP and sort bugs by your personal popcon ranking +(which is basically the atime of your packages' binaries). +.SH BUGS +It is not possible to say "does not apply only to unstable" +.SH SEE ALSO +.BR debtags(1) +.BR popbugs(1) +.BR popularity-contest(8) +.SH AUTHOR +\fBrc-alert\fR was written by Anthony DeRobertis and modified by +Julian Gilbey <jdg@debian.org> and Adam D. Barratt <adam@adam-barratt.org.uk> +for the devscripts package. Debtags and popcon functionality was added by Jan +Hauke Rahm <info@jhr-online.de>. diff --git a/scripts/rc-alert.pl b/scripts/rc-alert.pl new file mode 100755 index 0000000..7f3243d --- /dev/null +++ b/scripts/rc-alert.pl @@ -0,0 +1,501 @@ +#!/usr/bin/perl + +# rc-alert - find RC bugs for programs on your system +# Copyright (C) 2003 Anthony DeRobertis +# Modifications Copyright 2003 Julian Gilbey <jdg@debian.org> +# Modifications Copyright 2008 Adam D. Barratt <adam@adam-barratt.org.uk> +# Modifications copyright 2009 by Jan Hauke Rahm <info@jhr-online.de> +# +# 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 Devscripts::Packages; +use File::Basename; +use File::Copy qw(move); +use File::HomeDir; +use File::Path qw(make_path); +use File::Spec; +use Getopt::Long qw(:config bundling permute no_getopt_compat); + +sub remove_duplicate_values($); +sub store_if_relevant(%); +sub human_flags($); +sub unhtmlsanit($); +sub dt_parse_request($); + +$ENV{HOME} = File::HomeDir->my_home; +my $cachedir + = $ENV{XDG_CACHE_HOME} || File::Spec->catdir($ENV{HOME}, '.cache'); +$cachedir = File::Spec->catdir($cachedir, 'devscripts', 'rc-alert'); + +my $url = "http://bugs.debian.org/release-critical/other/all.html"; +my $cachefile = File::Spec->catfile($cachedir, basename($url)); +my $forcecache = 0; +my $usecache = 0; + +my @flags = ( + [qr/P/ => 'pending'], + [qr/\+/ => 'patch'], + [qr/H/ => 'help [wanted]'], + [qr/M/ => 'moreinfo [needed]'], + [qr/R/ => 'unreproducible'], + [qr/S/ => 'security'], + [qr/U/ => 'upstream'], +); +# A little hacky but allows us to sort the list by length +my @dists = ( + [qr/O/ => 'oldstable'], + [qr/S/ => 'stable'], + [qr/T/ => 'testing'], + [qr/U/ => 'unstable'], + [qr/E/ => 'experimental'], +); + +my $includetags = ""; +my $excludetags = ""; + +my $includedists = ""; +my $excludedists = ""; + +my $tagincoperation = "or"; +my $tagexcoperation = "or"; +my $distincoperation = "or"; +my $distexcoperation = "or"; + +my $popcon = 0; +my $popcon_by_vote = 0; +my $popcon_local = 0; + +my $debtags = ''; +my $debtags_db = '/var/lib/debtags/package-tags'; + +my $progname = basename($0); + +my $usage = <<"EOF"; +Usage: $progname [--help|--version|--cache] [package ...] + List all installed packages (or listed packages) with + release-critical bugs, as determined from the Debian + release-critical bugs list. + + Options: + --cache Create ~/.devscripts_cache directory if it does not exist + + Matching options: (see the manpage for further information) + --include-tags Set of tags to include + --include-tag-op Must all tags match for inclusion? + --exclude-tags Set of tags to exclude + --exclude-tag-op Must all tags match for exclusion? + --include-dists Set of distributions to include + --include-dist-op Must all distributions be matched for inclusion? + --exclude-dists Set of distributions to exclude + --exclude-dist-op Must all distributions be matched for exclusion? + + Debtags options: (only list packages with matching debtags) + --debtags Comma separated list of tags + (e.g. implemented-in::perl,role::plugin) + --debtags-database Database file (default: /var/lib/debtags/package-tags) + + Popcon options: + --popcon Sort bugs by package's popcon rank + --pc-vote Sort by_vote instead of by_inst + (see popularity-contest(8)) + --pc-local Use local popcon data from last popcon run + (/var/log/popularity-contest) +EOF + +my $version = <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +This code is copyright 2003 by Anthony DeRobertis +Modifications copyright 2003 by Julian Gilbey <jdg\@debian.org> +Modifications copyright 2008 by Adam D. Barratt <adam\@adam-barratt.org.uk> +Modifications copyright 2009 by Jan Hauke Rahm <info\@jhr-online.de> +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 + +## +## handle command-line options +## + +my ($opt_help, $opt_version); +GetOptions( + "help|h" => \$opt_help, + "version|v" => \$opt_version, + "cache" => \$forcecache, + "include-tags|f=s" => \$includetags, + "exclude-tags=s" => \$excludetags, + "include-tag-op|t=s" => \$tagincoperation, + "exclude-tag-op=s" => \$tagexcoperation, + "include-dists|d=s" => \$includedists, + "exclude-dists=s" => \$excludedists, + "include-dist-op|o=s" => \$distincoperation, + "exclude-dist-op=s" => \$distexcoperation, + "debtags=s" => \$debtags, + "debtags-database=s" => \$debtags_db, + "popcon" => \$popcon, + "pc-vote" => \$popcon_by_vote, + "pc-local" => \$popcon_local, +) or do { print $usage; exit 1; }; + +if ($opt_help) { print $usage; exit 0; } +if ($opt_version) { print $version; exit 0; } + +$tagincoperation =~ /^(or|and)$/ or $tagincoperation = 'or'; +$distincoperation =~ /^(or|and)$/ or $distincoperation = 'or'; +$tagexcoperation =~ /^(or|and)$/ or $tagexcoperation = 'or'; +$distexcoperation =~ /^(or|and)$/ or $distexcoperation = 'or'; +$includetags =~ s/[^P+HMRSUI]//gi; +$excludetags =~ s/[^P+HMRSUI]//gi; +$includedists =~ s/[^OSTUE]//gi; +$excludedists =~ s/[^OSTUE]//gi; +$includetags = remove_duplicate_values(uc($includetags)); +$excludetags = remove_duplicate_values(uc($excludetags)); +$includedists = remove_duplicate_values(uc($includedists)); +$excludedists = remove_duplicate_values(uc($excludedists)); + +## First download the RC bugs page + +my $curl_or_wget; +my $getcommand; +if (system("command -v wget >/dev/null 2>&1") == 0) { + $curl_or_wget = "wget"; + $getcommand = "wget -q -O -"; +} elsif (system("command -v curl >/dev/null 2>&1") == 0) { + $curl_or_wget = "curl"; + $getcommand = "curl -qfsL"; +} else { + die +"$progname: this program requires either the wget or curl package to be installed\n"; +} + +if (!-d $cachedir) { + if ($forcecache) { + make_path($cachedir); + } +} + +my $usingcache = 0; +if (-d $cachedir) { + chdir $cachedir or die "$progname: can't cd $cachedir: $!\n"; + + if ("$curl_or_wget" eq "wget") { + # Either use the cached version because the remote hasn't been + # updated (-N) or download a complete new copy (--no-continue) + if (system('wget', '-qN', '--no-continue', $url) != 0) { + die "$progname: wget failed!\n"; + } + } elsif ("$curl_or_wget" eq "curl") { + if (system('curl', '-qfsLR', $url) != 0) { + die "$progname: curl failed!\n"; + } + } else { + die "$progname: Unknown download program $curl_or_wget!\n"; + } + open BUGS, $cachefile or die "$progname: could not read $cachefile: $!\n"; + $usingcache = 1; +} else { + open BUGS, "$getcommand $url |" + or die "$progname: could not run $curl_or_wget: $!\n"; +} + +## Get list of installed packages (not source packages) +my $package_list; +if (@ARGV) { + my %tmp = map { $_ => 1 } @ARGV; + $package_list = \%tmp; +} else { + $package_list = InstalledPackages(1); +} + +## Get popcon information +my %popcon; +if ($popcon) { + my $pc_by = $popcon_by_vote ? 'vote' : 'inst'; + + my $pc_regex; + if ($popcon_local) { + open POPCON, "/var/log/popularity-contest" + or die "$progname: Unable to access popcon data: $!"; + $pc_regex = '(\d+)\s\d+\s(\S+)'; + } else { + open POPCON, + "$getcommand http://popcon.debian.org/by_$pc_by.gz | gunzip -c |" + or die "$progname: Not able to receive remote popcon data!"; + $pc_regex = '(\d+)\s+(\S+)\s+(\d+\s+){5}\(.*\)'; + } + + while (<POPCON>) { + next unless /$pc_regex/; + # rank $1 for package $2 + if ($popcon_local) { + # negative for inverse sorting of atimes + $popcon{$2} = "-$1"; + } else { + $popcon{$2} = $1; + } + } + close POPCON; +} + +## Get debtags info +my %dt_pkg; +my @dt_requests; +if ($debtags) { + ## read debtags database to %dt_pkg + open DEBTAGS, $debtags_db + or die "$progname: could not read debtags database: $!\n"; + while (<DEBTAGS>) { + next unless /^(.+?)(?::?\s*|:\s+(.+?)\s*)$/; + $dt_pkg{$1} = $2; + } + close DEBTAGS; + + ## and parse the request string + @dt_requests = dt_parse_request($debtags); +} + +## Read the list of bugs + +my $found_bugs_start; +my ($current_package, $comment); + +my $html; +{ + local $/; + $html = <BUGS>; +} + +my ($ignore) = $html =~ m%<strong>I</strong>: ([^<]*)%; +push(@flags, [qr/I/ => $ignore]); + +my @stanzas = $html =~ m%<div class="package">(.*?)</div>%gs; +my %pkg_store; +foreach my $stanza (@stanzas) { + if ($stanza + =~ m%<a name="([^\"]+)"><strong>Package:</strong></a> <a href="[^\"]+">%i + ) { + $current_package = $1; + $comment = ''; + while ($stanza + =~ m%<a name="(\d+)"></a>\s*<a href="[^\"]+">\d+</a> (\[[^\]]+\])( \[[^\]]+\])? ([^<]+)%igc + ) { + my ($num, $tags, $dists, $name) = ($1, $2, $3, $4); + chomp $name; + store_if_relevant( + pkg => $current_package, + num => $num, + tags => $tags, + dists => $dists, + name => $name, + comment => $comment + ); + } + } +} +for (sort { $a <=> $b } keys %pkg_store) { + print $pkg_store{$_}; +} + +if ($usingcache) { + close BUGS or die "$progname: could not close $cachefile: $!\n"; +} else { + close BUGS + or die $! + ? "$progname: could not close $curl_or_wget pipe: $!\n" + : "$progname: exit status from $curl_or_wget: $?\n"; +} + +exit 0; + +sub remove_duplicate_values($) { + my $in = shift || ""; + + $in = join("", sort { $a cmp $b } split //, $in); + + $in =~ s/(.)\1/$1/g while $in =~ /(.)\1/; + + return $in; +} + +sub store_if_relevant(%) { + my %args = @_; + + my $pkgname = $args{pkg}; + $args{pkg} =~ s/^src://; + + if ( exists($package_list->{ $args{pkg} }) + || exists($package_list->{$pkgname})) { + # potentially relevant + my ($flags, $flagsapply) = human_flags($args{tags}); + my $distsapply = 1; + my $dists; + ($dists, $distsapply) = human_dists($args{dists}) + if defined $args{dists}; + + return unless $flagsapply and $distsapply; + + foreach (@dt_requests) { + ## the array should be empty if nothing requested + return + unless ($dt_pkg{ $args{pkg} } + and $dt_pkg{ $args{pkg} } =~ /(\A|,\s*)$_(,|\z)/); + } + + # yep, relevant + my $bug_string + = "Package: $pkgname\n" + . $comment + . # non-empty comments always contain the trailing \n + "Bug: $args{num}\n" + . "Bug-URL: https://bugs.debian.org/$args{num}\n" + . "Title: " + . unhtmlsanit($args{name}) . "\n" + . "Flags: " + . $flags . "\n" + . (defined $args{dists} ? "Dists: " . $dists . "\n" : "") + . ( + defined $dt_pkg{ $args{pkg} } + ? "Debtags: " . $dt_pkg{ $args{pkg} } . "\n" + : "" + ); + + unless ($popcon_local) { + $bug_string .= ( + defined $popcon{ $args{pkg} } + ? "Popcon rank: " . $popcon{ $args{pkg} } . "\n" + : "" + ); + } + $bug_string .= "\n"; + + if ($popcon) { + return unless $bug_string; + my $index + = $popcon{ $args{pkg} } ? $popcon{ $args{pkg} } : 9999999; + $pkg_store{$index} .= $bug_string; + } else { + $pkg_store{1} .= $bug_string; + } + } +} + +sub human_flags($) { + my $mrf = shift; # machine readable flags, for those of you wondering + my @hrf = (); # considering above, should be obvious + my $matchedflags = 0; + my $matchedexcludes = 0; + my $applies = 1; + + foreach my $flagref (@flags) { + my ($flag, $desc) = @{$flagref}; + if ($mrf =~ $flag) { + if ($excludetags =~ $flag) { + $matchedexcludes++; + } elsif ($includetags =~ $flag or !$includetags) { + $matchedflags++; + } + push @hrf, $desc; + } + } + if ( $excludetags + and $tagexcoperation eq 'and' + and (length $excludetags == $matchedexcludes)) { + $applies = 0; + } elsif ($matchedexcludes and $tagexcoperation eq 'or') { + $applies = 0; + } elsif ($includetags and !$matchedflags) { + $applies = 0; + } elsif ($includetags + and $tagincoperation eq 'and' + and (length $includetags != $matchedflags)) { + $applies = 0; + } + + if (@hrf) { + return ("$mrf (" . join(", ", @hrf) . ')', $applies); + } else { + return ("$mrf (none)", $applies); + } +} + +sub human_dists($) { + my $mrf = shift; # machine readable flags, for those of you wondering + my @hrf = (); # considering above, should be obvious + my $matcheddists = 0; + my $matchedexcludes = 0; + my $applies = 1; + + foreach my $distref (@dists) { + my ($dist, $desc) = @{$distref}; + if ($mrf =~ $dist) { + if ($excludedists =~ $dist) { + $matchedexcludes++; + } elsif ($includedists =~ $dist or !$includedists) { + $matcheddists++; + } + push @hrf, $desc; + } + } + if ( $excludedists + and $distexcoperation eq 'and' + and (length $excludedists == $matchedexcludes)) { + $applies = 0; + } elsif ($matchedexcludes and $distexcoperation eq 'or') { + $applies = 0; + } elsif ($includedists and !$matcheddists) { + $applies = 0; + } elsif ($includedists + and $distincoperation eq 'and' + and (length $includedists != $matcheddists)) { + $applies = 0; + } + + if (@hrf) { + return ("$mrf (" . join(", ", @hrf) . ')', $applies); + } else { + return ('', $applies); + } +} + +# Reverse of master.debian.org:/srv/bugs.debian.org/cgi-bin/common.pl +sub unhtmlsanit ($) { + my %saniarray = ('lt', '<', 'gt', '>', 'amp', '&', 'quot', '"'); + my $in = $_[0]; + $in =~ s/&(lt|gt|amp|quot);/$saniarray{$1}/g; + return $in; +} + +sub dt_parse_request($) { + my %dt_lookup; + foreach (split /,/, $_[0]) { + my ($d_key, $d_val) = split '::', $_; + die +"$progname: A debtag must be of the form 'key::value'. See debtags(1) for details!" + unless ($d_key and $d_val); + if ($dt_lookup{$d_key}) { + $dt_lookup{$d_key} = "$dt_lookup{$d_key}|$d_val"; + } else { + $dt_lookup{$d_key} = quotemeta($d_val); + } + } + + my @out; + while (my ($dk, $dv) = each %dt_lookup) { + $dv = "($dv)" if ($dv =~ /\|/); + push @out, $dk . "::" . $dv; + } + return @out; +} diff --git a/scripts/reproducible-check b/scripts/reproducible-check new file mode 100755 index 0000000..498507d --- /dev/null +++ b/scripts/reproducible-check @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +# +# Copyright © 2017, 2020 Chris Lamb <lamby@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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# pylint: disable=invalid-name +# pylint: enable=invalid-name + +import argparse +import bz2 +import collections +import json +import logging +import os +import re +import subprocess +import sys +import time + +import apt +import requests + +try: + from xdg.BaseDirectory import xdg_cache_home +except ImportError: + print("This script requires the xdg python3 module.", file=sys.stderr) + print( + "Please install the python3-xdg Debian package in order to use this utility.", + file=sys.stderr, + ) + sys.exit(1) + + +class ReproducibleCheck: + HELP = """ + Reports on the reproducible status of installed packages. + For more details please see <https://reproducible-builds.org>. + """ + + NAME = os.path.basename(__file__) + VERSION = 1 + + STATUS_URL = "https://tests.reproducible-builds.org/debian/reproducible.json.bz2" + + CACHE = os.path.join(xdg_cache_home, NAME, os.path.basename(STATUS_URL)) + CACHE_AGE_SECONDS = 86400 + + @classmethod + def parse(cls): + parser = argparse.ArgumentParser(description=cls.HELP) + + parser.add_argument( + "-d", + "--debug", + help="show debugging messages", + default=False, + action="store_true", + ) + + parser.add_argument( + "-r", + "--raw", + help="print unreproducible binary packages only (for dd-list -i)", + default=False, + action="store_true", + ) + + parser.add_argument( + "--version", + help="print version and exit", + default=False, + action="store_true", + ) + + return cls(parser.parse_args()) + + def __init__(self, args): + self.args = args + + logging.basicConfig( + format="%(asctime).19s %(levelname).1s: %(message)s", + level=logging.DEBUG if args.debug else logging.INFO, + ) + + self.log = logging.getLogger() + + def main(self): + if self.args.version: + print(f"{self.NAME} version {self.VERSION}") + return 0 + + if self.get_distributor_id() != "Debian": + self.log.error("Refusing to return results for non-Debian distributions") + return 2 + + self.update_cache() + + installed = self.get_installed_packages() + reproducible = self.get_reproducible_packages() + + if self.args.raw: + self.output_raw(installed, reproducible) + else: + self.output_by_source(installed, reproducible) + + self.log.info( + "These results are based on data from the Reproducible Builds " + "CI framework, showing only the theoretical (and " + "unofficial) reproducibility of these Debian packages." + ) + + return 0 + + def get_distributor_id(self): + try: + distribution_id = ( + subprocess.check_output(("lsb_release", "-is")).decode("utf-8").strip() + ) + except subprocess.CalledProcessError: + distribution_id = "" + + self.log.debug("Detected distribution %s", distribution_id or "(unknown)") + + return distribution_id + + def update_cache(self): + self.log.debug("Checking cache file %s ...", self.CACHE) + + try: + if os.path.getmtime(self.CACHE) >= time.time() - self.CACHE_AGE_SECONDS: + self.log.debug("Cache is up to date") + return + except OSError: + pass + + new_cache = f"{self.CACHE}.new" + self.log.info("Updating cache to %s...", new_cache) + + response = requests.get(self.STATUS_URL, timeout=60) + + os.makedirs(os.path.dirname(self.CACHE), exist_ok=True) + + with open(new_cache, "wb") as f: + for content in response.iter_content(chunk_size=2**16): + f.write(content) + + os.rename(new_cache, self.CACHE) + + def get_reproducible_packages(self): + """ + Return (source, architecture, version) triplets for reproducible source + packages. + """ + + self.log.debug("Loading data from cache %s", self.CACHE) + + data = set() + source_packages = set() + + with bz2.open(self.CACHE) as f: + all_packages = json.loads(f.read().decode("utf-8")) + + for pkg in all_packages: + if pkg["status"] != "reproducible": + continue + + data.add((pkg["package"], pkg["architecture"], pkg["version"])) + + source_packages.add(pkg["package"]) + + self.log.debug("Parsed data about %d source packages", len(source_packages)) + + return data + + def get_installed_packages(self): + """ + Return (binary_package, architecture, version) triplets, mapped to + their corresponding source package. + """ + + result = {} + for pkg in apt.Cache(): + for pkg_ver in pkg.versions: + if not pkg_ver.is_installed: + continue + + # We may have installed a binNMU version locally so we need to + # strip these off when looking up against the JSON of results. + version = re.sub(r"\+b\d+$", "", pkg_ver.version) + + result[ + (pkg.shortname, pkg_ver.architecture, version) + ] = pkg_ver.source_name + + self.log.debug("Parsed %d installed binary packages", len(result)) + + return result + + def iter_installed_unreproducible(self, installed, reproducible): + # "Architecture: all" binary packages should pretend to the system's + # default architecture for lookup purposes. + default_architecture = apt.apt_pkg.config.find("APT::Architecture") + self.log.debug("Using %s as our 'Architecture: all' lookup") + + for (binary, architecture, version), source in sorted(installed.items()): + if architecture == "all": + architecture = default_architecture + + lookup_key = (source, architecture, version) + + if lookup_key not in reproducible: + yield binary, source, version + + def output_by_source(self, installed, reproducible): + by_source = collections.defaultdict(set) + + num_unreproducible = 0 + for binary, source, version in self.iter_installed_unreproducible( + installed, reproducible + ): + by_source[(source, version)].add(binary) + num_unreproducible += 1 + + for (source, version), binaries in sorted(by_source.items()): + # Calculate some clarifying suffixes/prefixes + src = "" + pkgs = "" + if binaries != {source}: + src = "src:" + pkgs = f" ({', '.join(binaries)})" + + print( + f"{src}{source} ({version}){pkgs} is not reproducible " + f"<https://tests.reproducible-builds.org/debian/{source}>" + ) + + num_installed = len(installed) + num_reproducible = len(installed) - num_unreproducible + percent = 100.0 * num_reproducible / num_installed + print( + f"{num_unreproducible}/{num_installed} ({percent:.2f}%) of " + f"installed binary packages are reproducible." + ) + + def output_raw(self, installed, reproducible): + for binary, _, _ in self.iter_installed_unreproducible(installed, reproducible): + print(binary) + + +if __name__ == "__main__": + try: + sys.exit(ReproducibleCheck.parse().main()) + except (KeyboardInterrupt, BrokenPipeError): + sys.exit(1) diff --git a/scripts/rmadison.pl b/scripts/rmadison.pl new file mode 100755 index 0000000..e60aead --- /dev/null +++ b/scripts/rmadison.pl @@ -0,0 +1,414 @@ +#!/usr/bin/perl +# vim: set ai shiftwidth=4 tabstop=4 expandtab: + +# Copyright (C) 2006-2013 Christoph Berg <myon@debian.org> +# (C) 2010 Uli Martens <uli@youam.net> +# +# 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 + +use strict; +use warnings; +use File::Basename; +use Getopt::Long qw(:config bundling permute no_getopt_compat); + +BEGIN { + pop @INC if $INC[-1] eq '.'; + # Load the URI::Escape module safely + eval { require URI::Escape; }; + if ($@) { + my $progname = basename $0; + if ($@ =~ /^Can\'t locate URI\/Escape\.pm/) { + die +"$progname: you must have the liburi-perl package installed\nto use this script\n"; + } + die +"$progname: problem loading the URI::Escape module:\n $@\nHave you installed the liburi-perl package?\n"; + } + import URI::Escape; +} + +my $VERSION = '0.4'; + +sub version($) { + my ($fd) = @_; + print $fd <<EOT; +rmadison $VERSION (devscripts ###VERSION###) +(C) 2006-2010 Christoph Berg <myon\@debian.org> +(C) 2010 Uli Martens <uli\@youam.net> +EOT +} + +my %url_map = ( + 'debian' => "https://api.ftp-master.debian.org/madison", + 'new' => "https://api.ftp-master.debian.org/madison?s=new", + 'qa' => "https://qa.debian.org/madison.php", + 'ubuntu' => "https://people.canonical.com/~ubuntu-archive/madison.cgi", + 'udd' => 'https://qa.debian.org/cgi-bin/madison.cgi', + 'archive' => 'https://qa.debian.org/cgi-bin/madison.cgi?table=archived', + 'ports' => 'https://qa.debian.org/cgi-bin/madison.cgi?table=ports', + 'janitor' => 'https://janitor.debian.net/api/madison', +); +my $default_url = 'debian'; +if (system('dpkg-vendor', '--is', 'ubuntu') == 0) { + $default_url = 'ubuntu'; +} + +sub usage($$) { + my ($fd, $exit) = @_; + my @urls = split /,/, $default_url; + my $url + = (@urls > 1) + ? join(', and ', join(', ', @urls[0 .. $#urls - 1]), $urls[-1]) + : $urls[0]; + + print $fd <<EOT; +Usage: rmadison [OPTION] PACKAGE[...] +Display information about PACKAGE(s). + + -a, --architecture=ARCH only show info for ARCH(s) + -b, --binary-type=TYPE only show info for binary TYPE + -c, --component=COMPONENT only show info for COMPONENT(s) + -g, --greaterorequal show buildd 'dep-wait pkg >= {highest version}' info + -G, --greaterthan show buildd 'dep-wait pkg >> {highest version}' info + -h, --help show this help and exit + -r, --regex treat PACKAGE as a regex [not supported everywhere] + -s, --suite=SUITE only show info for this suite + -S, --source-and-binary show info for the binary children of source pkgs + -t, --time show projectb snapshot date + -u, --url=URL use URL instead of $url + + --noconf, --no-conf don\'t read devscripts configuration files + +ARCH, COMPONENT and SUITE can be comma (or space) separated lists, e.g. + --architecture=amd64,i386 + +Aliases for URLs: +EOT + foreach my $alias (sort keys %url_map) { + print $fd "\t$alias\t$url_map{$alias}\n"; + } + exit $exit; +} + +my $params; +my $default_arch; +my $ssl_ca_file; +my $ssl_ca_path; + +if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) { + shift; +} else { + # We don't have any predefined variables, but allow any of the form + # RMADISON_URL_MAP_SHORTCODE=URL + my @config_files = ('/etc/devscripts.conf', '~/.devscripts'); + my @config_vars = (); + + my $shell_cmd; + # Set defaults + $shell_cmd .= qq[unset `set | grep "^RMADISON_" | cut -d= -f1`;\n]; + $shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n"; + $shell_cmd .= '[ -f $file ] && . $file; done;' . "\n"; + $shell_cmd .= 'for var in `set | grep "^RMADISON_" | cut -d= -f1`; do '; + $shell_cmd .= 'eval echo $var=\$$var; done;' . "\n"; + # Read back values + my $shell_out = `/bin/bash -c '$shell_cmd'`; + @config_vars = split /\n/, $shell_out, -1; + + foreach my $confvar (@config_vars) { + if ($confvar =~ /^RMADISON_URL_MAP_([^=]*)=(.*)$/) { + $url_map{ lc($1) } = $2; + } elsif ($confvar =~ /^RMADISON_DEFAULT_URL=(.*)$/) { + $default_url = $1; + } elsif ($confvar =~ /^RMADISON_ARCHITECTURE=(.*)$/) { + $default_arch = $1; + } elsif ($confvar =~ /^RMADISON_SSL_CA_FILE=(.*)$/) { + $ssl_ca_file = $1; + } elsif ($confvar =~ /^RMADISON_SSL_CA_PATH=(.*)$/) { + $ssl_ca_path = $1; + } + } +} + +unless ( + GetOptions( + '-a=s' => \$params->{'architecture'}, + '--architecture=s' => \$params->{'architecture'}, + '-b=s' => \$params->{'binary-type'}, + '--binary-type=s' => \$params->{'binary-type'}, + '-c=s' => \$params->{'component'}, + '--component=s' => \$params->{'component'}, + '-g' => \$params->{'greaterorequal'}, + '--greaterorequal' => \$params->{'greaterorequal'}, + '-G' => \$params->{'greaterthan'}, + '--greaterthan' => \$params->{'greaterthan'}, + '-h' => \$params->{'help'}, + '--help' => \$params->{'help'}, + '--noconf' => \$params->{'noconf'}, + '--no-conf' => \$params->{'noconf'}, + '-r' => \$params->{'regex'}, + '--regex' => \$params->{'regex'}, + '-s=s' => \$params->{'suite'}, + '--suite=s' => \$params->{'suite'}, + '-S' => \$params->{'source-and-binary'}, + '--source-and-binary' => \$params->{'source-and-binary'}, + '-t' => \$params->{'time'}, + '--time' => \$params->{'time'}, + '-u=s' => \$params->{'url'}, + '--url=s' => \$params->{'url'}, + '--version' => \$params->{'version'}, + ) +) { + usage(\*STDERR, 1); +} + +if ($params->{help}) { + usage(\*STDOUT, 0); +} +if ($params->{version}) { + version(\*STDOUT); + exit 0; +} +if ($params->{'noconf'}) { + print '--noconf must come first on the command line.'; + usage(\*STDOUT, 1); +} + +unless (@ARGV) { + print STDERR "E: need at least one package name as an argument.\n"; + exit 1; +} +if ($params->{greaterorequal} and $params->{greaterthan}) { + print STDERR + "E: -g/--greaterorequal and -G/--greaterthan are mutually exclusive.\n"; + exit 1; +} + +my @args; + +if ($params->{'architecture'}) { + push @args, "a=$params->{'architecture'}"; +} elsif ($default_arch) { + push @args, "a=$default_arch"; +} +push @args, "b=$params->{'binary-type'}" if $params->{'binary-type'}; +push @args, "c=$params->{'component'}" if $params->{'component'}; +push @args, "g" if $params->{'greaterorequal'}; +push @args, "G" if $params->{'greaterthan'}; +push @args, "r" if $params->{'regex'}; +push @args, "s=$params->{'suite'}" if $params->{'suite'}; +push @args, "S" if $params->{'source-and-binary'}; +push @args, "t" if $params->{'time'}; + +my $url = $params->{'url'} ? $params->{'url'} : $default_url; +my @url = split /,/, $url; + +my $status = 0; + +# Strip arch qualifiers from the package name, to help those that are feeding +# in output from other commands +s/:.*// for (@ARGV); + +foreach my $url (@url) { + print "$url:\n" if @url > 1; + $url = $url_map{$url} if $url_map{$url}; + my @cmd; + my @ssl_errors; + if (-x "/usr/bin/curl") { + @cmd = qw/curl -f -s -S -L/; + push @cmd, "--cacert", $ssl_ca_file if $ssl_ca_file; + push @cmd, "--capath", $ssl_ca_path if $ssl_ca_path; + push @ssl_errors, (60, 77); + } else { + @cmd = qw/wget -q -O -/; + push @cmd, "--ca-certificate=$ssl_ca_file" if $ssl_ca_file; + push @cmd, "--ca-directory=$ssl_ca_path" if $ssl_ca_path; + push @ssl_errors, 5; + } + system @cmd, + $url + . (($url =~ m/\?/) ? '&' : '?') + . "package=" + . join("+", map { uri_escape($_) } @ARGV) + . "&text=on&" + . join("&", @args); + my $rc = $? >> 8; + if ($rc != 0) { + if (grep { $_ == $rc } @ssl_errors) { + die +"Problem with SSL CACERT check:\n Have you installed the ca-certificates package?\n"; + } + $status = 1; + } +} + +exit $status; + +__END__ + +=head1 NAME + +rmadison -- Remotely query the Debian archive database about packages + +=head1 SYNOPSIS + +=over + +=item B<rmadison> [I<OPTIONS>] I<PACKAGE> ... + +=back + +=head1 DESCRIPTION + +B<dak ls> queries the Debian archive database ("projectb") and +displays which package version is registered per architecture/component/suite. +The CGI at B<https://qa.debian.org/madison.php> provides that service without +requiring SSH access to ftp-master.debian.org or the mirror on +mirror.ftp-master.debian.org. This script, B<rmadison>, is a command line +frontend to this CGI. + +=head1 OPTIONS + +=over + +=item B<-a>, B<--architecture=>I<ARCH> + +only show info for ARCH(s) + +=item B<-b>, B<--binary-type=>I<TYPE> + +only show info for binary TYPE + +=item B<-c>, B<--component=>I<COMPONENT> + +only show info for COMPONENT(s) + +=item B<-g>, B<--greaterorequal> + +show buildd 'dep-wait pkg >= {highest version}' info + +=item B<-G>, B<--greaterthan> + +show buildd 'dep-wait pkg >> {highest version}' info + +=item B<-h>, B<--help> + +show this help and exit + +=item B<-s>, B<--suite=>I<SUITE> + +only show info for this suite + +=item B<-r>, B<--regex> + +treat PACKAGE as a regex + +B<Note:> Since B<-r> can easily DoS the database ("-r ."), this option is not +supported by the CGI on qa.debian.org and most other installations. + +=item B<-S>, B<--source-and-binary> + +show info for the binary children of source pkgs + +=item B<-t>, B<--time> + +show projectb snapshot and reload time (not supported by all archives) + +=item B<-u>, B<--url=>I<URL>[B<,>I<URL> ...] + +use I<URL> for the query. Supported shorthands are + B<debian> https://api.ftp-master.debian.org/madison + B<new> https://api.ftp-master.debian.org/madison?s=new + B<qa> https://qa.debian.org/madison.php + B<ubuntu> https://people.canonical.com/~ubuntu-archive/madison.cgi + B<udd> https://qa.debian.org/cgi-bin/madison.cgi + B<archive> https://qa.debian.org/cgi-bin/madison.cgi?table=archived + B<ports> https://qa.debian.org/cgi-bin/madison.cgi?table=ports + +See the B<RMADISON_URL_MAP_> variable below for a method to add +new shorthands. + +=item B<--version> + +show version and exit + +=item B<--no-conf>, B<--noconf> + +don't read the devscripts configuration files + +=back + +ARCH, COMPONENT and SUITE can be comma (or space) separated lists, e.g. +--architecture=amd64,i386 + +=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<RMADISON_URL_MAP_>I<SHORTHAND>=I<URL> + +Add an entry to the set of shorthand URLs listed above. I<SHORTHAND> should +be replaced with the shorthand form to be used to refer to I<URL>. + +Multiple shorthand entries may be specified by using multiple +B<RMADISON_URL_MAP_*> variables. + +=item B<RMADISON_DEFAULT_URL>=I<URL> + +Set the default URL to use unless overridden by a command line option. +For Debian this defaults to debian. For Ubuntu this defaults to ubuntu. + +=item B<RMADISON_ARCHITECTURE>=I<ARCH> + +Set the default architecture to use unless overridden by a command line option. +To run an unrestricted query when B<RMADISON_ARCHITECTURE> is set, use +B<--architecture='*'>. + +=item B<RMADISON_SSL_CA_FILE>=I<FILE> + +Use the specified CA file instead of the default CA bundle for curl/wget, +passed as --cacert to curl, and as --ca-certificate to wget. + +=item B<RMADISON_SSL_CA_PATH>=I<PATH> + +Use the specified CA directory instead of the default CA bundle for curl/wget, +passed as --capath to curl, and as --ca-directory to wget. + +=back + +=head1 NOTES + +B<dak ls> was formerly called B<madison>. + +The protocol used by rmadison is fairly simple, the CGI accepts query the +parameters a, b, c, g, G, r, s, S, t, and package. The parameter text is passed to +enable plain-text output. + +=head1 SEE ALSO + +B<dak>(1), B<madison-lite>(1) + +=head1 AUTHOR + +rmadison and https://qa.debian.org/madison.php were written by Christoph Berg +<myon@debian.org>. dak was written by +James Troup <james@nocrew.org>, Anthony Towns <ajt@debian.org>, and others. + +=cut diff --git a/scripts/run_bisect.sh b/scripts/run_bisect.sh new file mode 100755 index 0000000..6238936 --- /dev/null +++ b/scripts/run_bisect.sh @@ -0,0 +1,114 @@ +#!/bin/sh +# +# Copyright 2020 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 script is part of debbisect and usually called by debbisect itself +# +# it accepts six or eight arguments: +# 1. dependencies +# 2. script name or shell snippet +# 3. mirror URL +# 4. architecture +# 5. suite +# 6. components +# 7. (optional) second mirror URL +# 8. (optional) package to upgrade +# +# It will create an ephemeral chroot using mmdebstrap using (3.) as mirror, +# (4.) as architecture, (5.) as suite and (6.) as components, install the +# dependencies given in (1.) and execute the script given in (2.). +# Its output is the exit code of the script as well as a file ./pkglist +# containing the output of "dpkg-query -W" inside the chroot. +# +# If not only six but eight arguments are given, then the second mirror URL +# (7.) will be added to the apt sources and the single package (8.) will be +# upgraded to its version from (7.). +# +# shellcheck disable=SC2016 + +set -exu + +if [ $# -ne 6 ] && [ $# -ne 8 ]; then + echo "usage: $0 depends script mirror1 architecture suite components [mirror2 toupgrade]" + exit 1 +fi + +depends=$1 +script=$2 +mirror1=$3 +architecture=$4 +suite=$5 +components=$6 + +# The following hacks are needed to go back as far as 2006-08-10: +# +# - Acquire::Check-Valid-Until "false" allows Release files with an expired +# Valid-Until dates +# - Apt::Key::gpgvcommand allows expired GPG keys +# - Apt::Hashes::SHA1::Weak "yes" allows GPG keys with weak SHA1 signature +# - /usr/share/keyrings lets apt use debian-archive-removed-keys.gpg +# - /usr/share/mmdebstrap/hooks/jessie-or-older performs some setup that is +# only required for Debian Jessie or older +# +if [ $# -eq 6 ]; then + mmdebstrap \ + --verbose \ + --aptopt='Acquire::Check-Valid-Until "false"' \ + --aptopt='Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"' \ + --aptopt='Apt::Hashes::SHA1::Weak "yes"' \ + --keyring=/usr/share/keyrings \ + --hook-dir=/usr/share/mmdebstrap/hooks/maybe-jessie-or-older \ + --hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr \ + --skip=check/signed-by \ + --variant=apt \ + --components="$components" \ + --include="$depends" \ + --architecture="$architecture" \ + --customize-hook='chroot "$1" sh -c "dpkg-query -W > /pkglist"' \ + --customize-hook='download /pkglist ./debbisect.'"$DEBIAN_BISECT_TIMESTAMP"'.pkglist' \ + --customize-hook='rm "$1"/pkglist' \ + --customize-hook='chroot "$1" dpkg-query --list --no-pager' \ + --customize-hook="$script" \ + "$suite" \ + - \ + "$mirror1" \ + >/dev/null +elif [ $# -eq 8 ]; then + mirror2=$7 + toupgrade=$8 + mmdebstrap \ + --verbose \ + --aptopt='Acquire::Check-Valid-Until "false"' \ + --aptopt='Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"' \ + --aptopt='Apt::Hashes::SHA1::Weak "yes"' \ + --keyring=/usr/share/keyrings \ + --hook-dir=/usr/share/mmdebstrap/hooks/maybe-jessie-or-older \ + --hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr \ + --skip=check/signed-by \ + --variant=apt \ + --components="$components" \ + --include="$depends" \ + --architecture="$architecture" \ + --customize-hook='echo "deb '"$mirror2 $suite $(echo "$components" | tr ',' ' ')"'" > "$1"/etc/apt/sources.list' \ + --customize-hook='chroot "$1" apt-get update' \ + --customize-hook='chroot "$1" env DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true apt-get --yes install --no-install-recommends '"$toupgrade" \ + --customize-hook='chroot "$1" sh -c "dpkg-query -W > /pkglist"' \ + --customize-hook='download /pkglist ./debbisect.'"$DEBIAN_BISECT_TIMESTAMP.$toupgrade"'.pkglist' \ + --customize-hook='rm "$1"/pkglist' \ + --customize-hook='chroot "$1" dpkg-query --list --no-pager' \ + --customize-hook="$script" \ + "$suite" \ + - \ + "$mirror1" \ + >/dev/null +fi diff --git a/scripts/run_bisect_qemu.sh b/scripts/run_bisect_qemu.sh new file mode 100755 index 0000000..d0f8dcc --- /dev/null +++ b/scripts/run_bisect_qemu.sh @@ -0,0 +1,365 @@ +#!/bin/sh +# +# Copyright 2020 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 script is part of debbisect and usually called by debbisect itself +# +# it accepts eight or ten arguments: +# 1. dependencies +# 2. script name or shell snippet +# 3. mirror URL +# 4. architecture +# 5. suite +# 6. components +# 7. memsize +# 8. disksize +# 9. (optional) second mirror URL +# 10. (optional) package to upgrade +# +# It will create an ephemeral qemu virtual machine using mmdebstrap and +# guestfish using (3.) as mirror, (4.) as architecture, (5.) as suite and +# (6.) as components, install the dependencies given in (1.) and execute the +# script given in (2.). +# Its output is the exit code of the script as well as a file ./pkglist +# containing the output of "dpkg-query -W" inside the chroot. +# +# If not only six but eight arguments are given, then the second mirror URL +# (9.) will be added to the apt sources and the single package (10.) will be +# upgraded to its version from (9.). +# +# shellcheck disable=SC2016 + +set -exu + +if [ $# -ne 8 ] && [ $# -ne 10 ]; then + echo "usage: $0 depends script mirror1 architecture suite components memsize disksize [mirror2 toupgrade]" + exit 1 +fi + +depends=$1 +script=$2 +mirror1=$3 +architecture=$4 +suite=$5 +components=$6 +memsize=$7 +disksize=$8 + +if [ $# -eq 10 ]; then + mirror2=$9 + toupgrade=${10} +fi + +case $architecture in + alpha) qemuarch=alpha;; + amd64) qemuarch=x86_64;; + arm) qemuarch=arm;; + arm64) qemuarch=aarch64;; + armel) qemuarch=arm;; + armhf) qemuarch=arm;; + hppa) qemuarch=hppa;; + i386) qemuarch=i386;; + m68k) qemuarch=m68k;; + mips) qemuarch=mips;; + mips64) qemuarch=mips64;; + mips64el) qemuarch=mips64el;; + mipsel) qemuarch=mipsel;; + powerpc) qemuarch=ppc;; + ppc64) qemuarch=ppc64;; + ppc64el) qemuarch=ppc64le;; + riscv64) qemuarch=riscv64;; + s390x) qemuarch=s390x;; + sh4) qemuarch=sh4;; + sparc) qemuarch=sparc;; + sparc64) qemuarch=sparc64;; + *) echo "no qemu support for $architecture"; exit 1;; +esac +case $architecture in + i386) linuxarch=686-pae;; + amd64) linuxarch=amd64;; + arm64) linuxarch=arm64;; + armhf) linuxarch=armmp;; + ia64) linuxarch=itanium;; + m68k) linuxarch=m68k;; + armel) linuxarch=marvell;; + hppa) linuxarch=parisc;; + powerpc) linuxarch=powerpc;; + ppc64) linuxarch=powerpc64;; + ppc64el) linuxarch=powerpc64le;; + riscv64) linuxarch=riscv64;; + s390x) linuxarch=s390x;; + sparc64) linuxarch=sparc64;; + *) echo "no kernel image for $architecture"; exit 1;; +esac + +TMPDIR=$(mktemp --tmpdir --directory debbisect_qemu.XXXXXXXXXX) +cleantmp() { + for f in customize.sh id_rsa id_rsa.pub qemu.log config; do + rm -f "$TMPDIR/$f" + done + rmdir "$TMPDIR" +} + +trap cleantmp EXIT +# the temporary directory must be world readable (for example in unshare mode) +chmod a+xr "$TMPDIR" + +ssh-keygen -q -t rsa -f "$TMPDIR/id_rsa" -N "" + +cat << SCRIPT > "$TMPDIR/customize.sh" +#!/bin/sh +set -exu + +rootfs="\$1" + +# setup various files in /etc +echo host > "\$rootfs/etc/hostname" +echo "127.0.0.1 localhost host" > "\$rootfs/etc/hosts" +echo "/dev/vda1 / auto errors=remount-ro 0 1" > "\$rootfs/etc/fstab" +cat /etc/resolv.conf > "\$rootfs/etc/resolv.conf" + +# setup users +chroot "\$rootfs" passwd --delete root +chroot "\$rootfs" useradd --home-dir /home/user --create-home user +chroot "\$rootfs" passwd --delete user + +# extlinux config to boot from /dev/vda1 with predictable network interface +# naming and a serial console for logging +cat << END > "\$rootfs/extlinux.conf" +default linux +timeout 0 + +label linux +kernel /vmlinuz +append initrd=/initrd.img root=/dev/vda1 net.ifnames=0 console=ttyS0 +END + +# network interface config +# we can use eth0 because we boot with net.ifnames=0 for predictable interface +# names +cat << END > "\$rootfs/etc/network/interfaces" +auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp +END + +# copy in the public key +mkdir "\$rootfs/root/.ssh" +cp "$TMPDIR/id_rsa.pub" "\$rootfs/root/.ssh/authorized_keys" +chroot "\$rootfs" chown 0:0 /root/.ssh/authorized_keys +SCRIPT +chmod +x "$TMPDIR/customize.sh" + +# The following hacks are needed to go back as far as 2006-08-10: +# +# - Acquire::Check-Valid-Until "false" allows Release files with an expired +# Valid-Until dates +# - Apt::Key::gpgvcommand allows expired GPG keys +# - Apt::Hashes::SHA1::Weak "yes" allows GPG keys with weak SHA1 signature +# - /usr/share/keyrings lets apt use debian-archive-removed-keys.gpg +# - /usr/share/mmdebstrap/hooks/jessie-or-older performs some setup that is +# only required for Debian Jessie or older +# +mmdebstrap --architecture="$architecture" --verbose --variant=apt --components="$components" \ + --aptopt='Acquire::Check-Valid-Until "false"' \ + --aptopt='Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"' \ + --aptopt='Apt::Hashes::SHA1::Weak "yes"' \ + --keyring=/usr/share/keyrings \ + --hook-dir=/usr/share/mmdebstrap/hooks/maybe-jessie-or-older \ + --hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr \ + --skip=check/signed-by \ + --include='openssh-server,systemd-sysv,ifupdown,netbase,isc-dhcp-client,udev,policykit-1,linux-image-'"$linuxarch" \ + --customize-hook="$TMPDIR/customize.sh" \ + "$suite" debian-rootfs.tar "$mirror1" + +# use guestfish to prepare the host system +# +# - create a single 4G partition and unpack the rootfs tarball into it +# - unpack the tarball of the container into / +# - put a syslinux MBR into the first 440 bytes of the drive +# - install extlinux and make partition bootable +# +# useful stuff to debug any errors: +# LIBGUESTFS_BACKEND_SETTINGS=force_tcg +# libguestfs-test-tool || true +# export LIBGUESTFS_DEBUG=1 LIBGUESTFS_TRACE=1 +guestfish -N "debian-rootfs.img=disk:$disksize" -- \ + part-disk /dev/sda mbr : \ + mkfs ext4 /dev/sda1 : \ + mount /dev/sda1 / : \ + tar-in "debian-rootfs.tar" / : \ + upload /usr/lib/SYSLINUX/mbr.bin /mbr.bin : \ + copy-file-to-device /mbr.bin /dev/sda size:440 : \ + rm /mbr.bin : \ + extlinux / : \ + sync : \ + umount / : \ + part-set-bootable /dev/sda 1 true : \ + shutdown + + +# start the host system +# prefer using kvm but fall back to tcg if not available +# avoid entropy starvation by feeding the crypt system with random bits from /dev/urandom +# the default memory size of 128 MiB is not enough for Debian, so we go with 1G +# use a virtio network card instead of emulating a real network device +# we don't need any graphics +# this also multiplexes the console and the monitor to stdio +# creates a multiplexed stdio backend connected to the serial port and the qemu +# monitor +# redirect tcp connections on port 10022 localhost to the host system port 22 +# redirect all output to a file +# run in the background +timeout --kill-after=60s 60m \ + qemu-system-"$qemuarch" \ + -M accel=kvm:tcg \ + -no-user-config \ + -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0 \ + -m "$memsize" \ + -net nic,model=virtio \ + -nographic \ + -serial mon:stdio \ + -net user,hostfwd=tcp:127.0.0.1:10022-:22 \ + -drive file="debian-rootfs.img",format=raw,if=virtio \ + > "$TMPDIR/qemu.log" </dev/null 2>&1 & + +# store the pid +QEMUPID=$! + +# use a function here, so that we can properly quote the path to qemu.log +showqemulog() { + cat --show-nonprinting "$TMPDIR/qemu.log" +} + +# show the log and kill qemu in case the script exits first +trap 'showqemulog; cleantmp; kill $QEMUPID' EXIT + +# the default ssh command does not store known hosts and even ignores host keys +# it identifies itself with the rsa key generated above +# pseudo terminal allocation is disabled or otherwise, programs executed via +# ssh might wait for input on stdin of the ssh process + +cat << END > "$TMPDIR/config" +Host qemu + Hostname 127.0.0.1 + User root + Port 10022 + UserKnownHostsFile /dev/null + StrictHostKeyChecking no + IdentityFile $TMPDIR/id_rsa + RequestTTY no +END + +TIMESTAMP=$(sleepenh 0 || [ $? -eq 1 ]) +TIMEOUT=5 +NUM_TRIES=40 +i=0 +while true; do + rv=0 + ssh -F "$TMPDIR/config" -o ConnectTimeout=$TIMEOUT qemu echo success || rv=1 + [ $rv -eq 0 ] && break + # if the command before took less than $TIMEOUT seconds, wait the remaining time + TIMESTAMP=$(sleepenh "$TIMESTAMP" "$TIMEOUT" || [ $? -eq 1 ]); + i=$((i+1)) + if [ $i -ge $NUM_TRIES ]; then + break + fi +done + +if [ $i -eq $NUM_TRIES ]; then + echo "timeout reached: unable to connect to qemu via ssh" + exit 1 +fi + +# if any url in sources.list points to 127.0.0.1 then we have to replace them +# by the host IP as seen by the qemu guest +cat << SCRIPT | ssh -F "$TMPDIR/config" qemu sh +set -eu +if [ -e /etc/apt/sources.list ]; then + sed -i 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/' /etc/apt/sources.list +fi +find /etc/apt/sources.list.d -type f -name '*.list' -print0 \ + | xargs --null --no-run-if-empty sed -i 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/' +SCRIPT + +# we install dependencies now and not with mmdebstrap --include in case some +# dependencies require a full system present +if [ -n "$depends" ]; then + ssh -F "$TMPDIR/config" qemu apt-get update + # shellcheck disable=SC2046,SC2086 + ssh -F "$TMPDIR/config" qemu env DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true apt-get --yes install --no-install-recommends $(echo $depends | tr ',' ' ') +fi + +# in its ten-argument form, a single package has to be upgraded to its +# version from the first bad timestamp +if [ $# -eq 10 ]; then + # replace content of sources.list with first bad timestamp + mirror2=$(echo "$mirror2" | sed 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/') + echo "deb $mirror2 $suite $(echo "$components" | tr ',' ' ')" | ssh -F "$TMPDIR/config" qemu "cat > /etc/apt/sources.list" + ssh -F "$TMPDIR/config" qemu apt-get update + # upgrade a single package (and whatever else apt deems necessary) + before=$(ssh -F "$TMPDIR/config" qemu dpkg-query -W) + ssh -F "$TMPDIR/config" qemu env DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true apt-get --yes install --no-install-recommends "$toupgrade" + after=$(ssh -F "$TMPDIR/config" qemu dpkg-query -W) + # make sure that something was upgraded + if [ "$before" = "$after" ]; then + echo "nothing got upgraded -- this should never happen" >&2 + exit 1 + fi + ssh -F "$TMPDIR/config" qemu dpkg-query -W > "./debbisect.$DEBIAN_BISECT_TIMESTAMP.$toupgrade.pkglist" +else + ssh -F "$TMPDIR/config" qemu dpkg-query -W > "./debbisect.$DEBIAN_BISECT_TIMESTAMP.pkglist" +fi + +ssh -F "$TMPDIR/config" qemu dpkg-query --list -no-pager + +# explicitly export all necessary variables +# because we use set -u this also makes sure that this script has these +# variables set in the first place +export DEBIAN_BISECT_EPOCH="$DEBIAN_BISECT_EPOCH" +export DEBIAN_BISECT_TIMESTAMP="$DEBIAN_BISECT_TIMESTAMP" +if [ -z ${DEBIAN_BISECT_MIRROR+x} ]; then + # DEBIAN_BISECT_MIRROR was unset (caching is disabled) + true +else + # replace the localhost IP by the IP of the host as seen by qemu + DEBIAN_BISECT_MIRROR=$(echo "$DEBIAN_BISECT_MIRROR" | sed 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/') + export DEBIAN_BISECT_MIRROR="$DEBIAN_BISECT_MIRROR" +fi + + +# either execute $script as a script from $PATH or as a shell snippet +ret=0 +if [ -x "$script" ] || echo "$script" | grep --invert-match --silent --perl-regexp '[^\w@\%+=:,.\/-]'; then + "$script" "$TMPDIR/config" || ret=$? +else + sh -c "$script" exec "$TMPDIR/config" || ret=$? +fi + +# since we installed systemd-sysv, systemctl is available +ssh -F "$TMPDIR/config" qemu systemctl poweroff + +wait $QEMUPID + +trap - EXIT + +showqemulog +cleantmp + +if [ "$ret" -eq 0 ]; then + exit 0 +else + exit 1 +fi diff --git a/scripts/sadt b/scripts/sadt new file mode 100755 index 0000000..b874330 --- /dev/null +++ b/scripts/sadt @@ -0,0 +1,647 @@ +#!/usr/bin/python3 +# encoding=UTF-8 + +# Copyright © 2012, 2013, 2014 Jakub Wilk <jwilk@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. + +""" +simple DEP-8 test runner +""" + +import argparse +import errno +import os +import queue as queuemod +import re +import shutil +import stat +import subprocess as ipc +import sys +import tempfile +import threading +import warnings + +from debian import deb822 + + +def parse_relations(field): + """ + wrap debian.deb822.PkgRelation.parse_relations() to suppress the + UserWarning about the inability to parse something. + See https://bugs.debian.org/712513 + """ + warnings.simplefilter("ignore") + parsed = deb822.PkgRelation.parse_relations(field) + warnings.resetwarnings() + return parsed + + +def chmod_x(path): + """ + chmod a+X <path> + """ + old_mode = stat.S_IMODE(os.stat(path).st_mode) + new_mode = old_mode | ((old_mode & 0o444) >> 2) + if old_mode != new_mode: + os.chmod(path, new_mode) + return old_mode + + +def annotate_output(child): + queue = queuemod.Queue() + + def reader(fd, tag): + buf = b"" + while True: + assert b"\n" not in buf + chunk = os.read(fd, 1024) + if chunk == b"": + break + lines = (buf + chunk).split(b"\n") + buf = lines.pop() + for line in lines: + queue.put((tag, line + b"\n")) + if buf != b"": + queue.put((tag, buf)) + queue.put(None) + + queue = queuemod.Queue() + threads = [] + for pipe, tag in [(child.stdout, "O"), (child.stderr, "E")]: + thread = threading.Thread(target=reader, args=(pipe.fileno(), tag)) + thread.start() + threads += [thread] + nthreads = len(threads) + while nthreads > 0: + item = queue.get() + if item is None: + nthreads -= 1 + continue + yield item + for thread in threads: + thread.join() + + +class Skip(Exception): + pass + + +class Flaky(Exception): + pass + + +class Fail(Exception): + pass + + +class Progress: + @staticmethod + def _write(text): + sys.stdout.write(text) + sys.stdout.flush() + + def start(self, name): + pass + + def output(self, line): + pass + + def skip(self, name, reason): + raise NotImplementedError + + def fail(self, name, reason): + raise NotImplementedError + + def ok(self, name): + raise NotImplementedError + + def close(self): + pass + + +class DefaultProgress(Progress): + _hourglass = r"/-\|" + + def __init__(self): + self._counter = 0 + self._output = False + if sys.stdout.isatty(): + self._back = "\b" + else: + self._back = "" + + def _reset(self): + if self._output: + self._write(self._back) + + def start(self, name): + self._output = False + + def output(self, line): + if not self._back: + return + hourglass = self._hourglass + counter = self._counter + 1 + self._reset() + self._write(hourglass[counter % len(hourglass)]) + self._counter = counter + self._output = True + + def skip(self, name, reason): + self._write("S") + + def fail(self, name, reason): + self._reset() + self._write("F") + + def ok(self, name): + self._reset() + self._write(".") + + def close(self): + self._write("\n") + + +class VerboseProgress(Progress): + def _separator(self): + self._write("-" * 70 + "\n") + + def start(self, name): + self._separator() + self._write(f"{name}\n") + self._separator() + + def output(self, line): + self._write(line) + + def skip(self, name, reason): + self._write(f"{name}: SKIP ({reason})\n") + + def fail(self, name, reason): + self._separator() + self._write(f"{name}: FAIL ({reason})\n") + self._write("\n") + + def ok(self, name): + self._separator() + self._write(f"{name}: PASS\n") + self._write("\n") + + +class TestCommand: + def __init__(self, group, command): + self.group = group + self.command = command + + def __str__(self): + return self.command + + @property + def name(self): + return str(self) + + def get_command(self): + return ["sh", "-c", self.command] + + def prepare(self, progress, rw_build_tree): + pass + + def cleanup(self): + pass + + def run(self, progress, options): # pylint: disable=too-many-locals + command = self.get_command() + tmpdir1 = tempfile.mkdtemp(prefix="sadt.") + tmpdir2 = tempfile.mkdtemp(prefix="sadt.") + environ = dict(os.environ) + environ["AUTOPKGTEST_TMP"] = tmpdir1 + # only for compatibility with old DEP-8 spec. + environ["ADTTMP"] = tmpdir2 + child = ipc.Popen( # pylint: disable=consider-using-with + command, stdout=ipc.PIPE, stderr=ipc.PIPE, env=environ + ) + output = [] + stderr = False + for tag, line in annotate_output(child): + if tag == "E": + stderr = True + this_line = f"{tag}: {line.decode(sys.stdout.encoding, 'replace')}" + progress.output(this_line) + output.append(this_line) + for fp in child.stdout, child.stderr: + fp.close() + returncode = child.wait() + shutil.rmtree(tmpdir1) + shutil.rmtree(tmpdir2) + + if returncode == 77 and options.skippable: + reason = "exit status 77 and marked as skippable" + progress.skip(self, reason) + raise Skip(self, reason, "".join(output)) + + fail_reason = None + + if returncode == 0: + if stderr and not options.allow_stderr: + returncode = -1 + fail_reason = "stderr non-empty" + else: + fail_reason = f"exit code: {returncode}" + + if returncode != 0: + if options.flaky: + progress.skip(self, fail_reason) + raise Flaky(self, fail_reason, "".join(output)) + + progress.fail(self, fail_reason) + raise Fail(self, fail_reason, "".join(output)) + + progress.ok(self) + + +class Test(TestCommand): + def __init__(self, group, testname): + self.testname = testname + self.original_mode = None + self.cwd = None + super().__init__(group, testname) + + @property + def path(self): + return os.path.join(self.group.tests_directory, self.testname) + + def __str__(self): + return self.testname + + def get_command(self): + return [self.path] + + def prepare(self, progress, rw_build_tree): + if rw_build_tree: + self.cwd = os.getcwd() + os.chdir(rw_build_tree) + chmod_x(self.path) + else: + if not os.access(self.path, os.X_OK): + try: + self.original_mode = chmod_x(self.path) + except OSError as exc: + progress.skip( + self.testname, + f"{self.path} could not be made executable: {exc}", + ) + raise Skip from exc + + def cleanup(self): + if self.original_mode is not None: + os.chmod(self.path, self.original_mode) + if self.cwd is not None: + os.chdir(self.cwd) + + +class TestOptions: # pylint: disable=too-few-public-methods + def __init__(self): + self.allow_stderr = False + self.flaky = False + self.rw_build_tree_needed = False + self.skippable = False + + +class TestGroup: + def __init__(self): + self.tests = [] + self.restrictions = frozenset() + self.features = frozenset() + self.depends = "@" + self.tests_directory = "debian/tests" + self._depends_checked = False + self._depends_cache = None + + def __iter__(self): + return iter(self.tests) + + def expand_depends(self, packages, build_depends): + if "@" not in self.depends: + return + or_clauses = [] + parsed_depends = parse_relations(self.depends) + for or_clause in parsed_depends: + if len(or_clause) == 1 and or_clause[0]["name"] == "@builddeps@": + or_clauses += build_depends + or_clauses += parse_relations("make") + continue + stripped_or_clause = [r for r in or_clause if r["name"] != "@"] + if len(stripped_or_clause) < len(or_clause): + for package in packages: + or_clauses += [ + stripped_or_clause + + [{"name": package, "version": None, "arch": None}] + ] + else: + or_clauses += [or_clause] + self.depends = deb822.PkgRelation.str(or_clauses) + + def check_depends(self): + if self._depends_checked: + if isinstance(self._depends_cache, Exception): + raise self._depends_cache # fpos, pylint: disable=raising-bad-type + return + child = ipc.Popen( # pylint: disable=consider-using-with + ["dpkg-checkbuilddeps", "-d", self.depends], stderr=ipc.PIPE, env={} + ) + error = child.stderr.read().decode("ASCII") + child.stderr.close() + if child.wait() != 0: + error = re.sub( + "^dpkg-checkbuilddeps: Unmet build dependencies", + "unmet dependencies", + error, + ) + error = error.rstrip() + skip = Skip(error) + self._depends_cache = skip + raise skip + self._depends_checked = True + + def check_restrictions(self, ignored_restrictions): + options = TestOptions() + restrictions = self.restrictions - frozenset(ignored_restrictions) + + for restriction in restrictions: + if restriction == "rw-build-tree": + options.rw_build_tree_needed = True + elif restriction == "needs-root": + if os.getuid() != 0: + raise Skip("this test needs root privileges") + elif restriction == "breaks-testbed": + raise Skip("breaks-testbed restriction is not implemented; use adt-run") + elif restriction == "build-needed": + raise Skip("source tree not built") + elif restriction == "allow-stderr": + options.allow_stderr = True + elif restriction == "flaky": + options.flaky = True + elif restriction == "skippable": + options.skippable = True + else: + raise Skip(f"unknown restriction: {restriction}") + return options + + def check(self, ignored_restrictions=()): + options = self.check_restrictions(ignored_restrictions) + self.check_depends() + return options + + def run( + self, + test, + progress, + ignored_restrictions=(), + rw_build_tree=None, + built_source_tree=None, + ): + ignored_restrictions = set(ignored_restrictions) + if rw_build_tree: + ignored_restrictions.add("rw-build-tree") + if built_source_tree: + ignored_restrictions.add("build-needed") + ignored_restrictions.add("needs-recommends") + ignored_restrictions.add("superficial") + try: + options = self.check(ignored_restrictions) + except Skip as exc: + progress.skip(test, str(exc)) + raise + test.prepare(progress, rw_build_tree) + try: + progress.start(test) + test.run(progress, options) + finally: + test.cleanup() + + def add_tests(self, tests): + tests = [Test(self, t) for t in re.split(r"\s*,?\s+", tests)] + self.tests = frozenset(tests) + + def add_test_command(self, test_command): + self.tests = frozenset([TestCommand(self, test_command)]) + + def add_restrictions(self, restrictions): + restrictions = re.split(r"\s*,?\s+", restrictions) + self.restrictions = frozenset(restrictions) + + def add_features(self, features): + features = re.split(r"\s*,?\s+", features) + self.features = frozenset(features) + + def add_depends(self, depends): + self.depends = depends + + def add_tests_directory(self, path): + self.tests_directory = path + + +def copy_build_tree(): + rw_build_tree = tempfile.mkdtemp(prefix="sadt-rwbt.") + print(f"sadt: info: copying build tree to {rw_build_tree}", file=sys.stderr) + ipc.check_call(["cp", "-a", ".", rw_build_tree]) + return rw_build_tree + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__.strip()) + parser.add_argument("-v", "--verbose", action="store_true", help="verbose output") + parser.add_argument( + "-b", + "--built-source-tree", + action="store_true", + help="assume built source tree", + ) + parser.add_argument( + "--run-autodep8", action="store_true", help="Run autodep8 (default)" + ) + parser.add_argument( + "--no-run-autodep8", action="store_false", help="Don't run autodep8" + ) + parser.set_defaults(run_autodep8=True) + parser.add_argument( + "--ignore-restrictions", + metavar="<restr>[,<restr>...]", + help="ignore specified restrictions", + default="", + ) + parser.add_argument("tests", metavar="<test-name>", nargs="*", help="tests to run") + options = parser.parse_args() + options.tests = frozenset(options.tests) + options.ignore_restrictions = frozenset(options.ignore_restrictions.split(",")) + return options + + +def get_test_groups( + binary_packages, build_depends, run_autodep8: bool +) -> list[TestGroup]: + test_groups = [] + try: + ipc.check_call(["which", "autodep8"], stdout=ipc.DEVNULL) + autodep8_available = True + except ipc.CalledProcessError: + autodep8_available = False + try: + if run_autodep8 and autodep8_available: + file = tempfile.TemporaryFile("w+") + ipc.check_call(["autodep8"], stdout=file) + file.seek(0) + else: + file = open("debian/tests/control", encoding="UTF-8") + except IOError as exc: + if exc.errno == errno.ENOENT: + print("sadt: error: cannot find debian/tests/control", file=sys.stderr) + sys.exit(1) + raise + with file: + for para in deb822.Packages.iter_paragraphs(file): + group = TestGroup() + for key, value in para.items(): + lkey = key.lower().replace("-", "_") + try: + method = getattr(group, "add_" + lkey) + except AttributeError: + print( + f"sadt: warning: unknown field {key}," + f" skipping the whole paragraph", + file=sys.stderr, + ) + group = None + break + method(value) + if group is not None: + group.expand_depends(binary_packages, build_depends) + test_groups += [group] + return test_groups + + +def test_summary(failures, flakes, n_skip: int, n_ok: int) -> str: + n_fail = len(failures) + n_flake = len(flakes) + n_test = n_fail + n_flake + n_skip + n_ok + if failures: + for name, exception in failures: + print("=" * 70) + print(f"FAIL: {name} ({exception.args[1]})") + if flakes: + for name, exception in flakes: + print("=" * 70) + print(f"FLAKY: {name} ({exception.args[1]})") + print() + fmt_message = [f"tests={n_test}"] + if n_skip > 0: + fmt_message += [f"skipped={n_skip}"] + if n_fail > 0: + fmt_message += [f"failures={n_fail}"] + if n_flake > 0: + fmt_message += [f"flaky={n_flake}"] + if fmt_message: + extra_message = f" ({', '.join(fmt_message)})" + else: + extra_message = "" + message = ("OK" if n_fail == 0 else "FAILED") + extra_message + return message + + +def run_tests(test_groups: list[TestGroup], options: argparse.Namespace) -> None: + # TODO: refactor run_tests function + # pylint: disable=too-many-branches,too-many-nested-blocks + failures = [] + flakes = [] + n_skip = n_ok = 0 + progress = VerboseProgress() if options.verbose else DefaultProgress() + rw_build_tree = None + try: + for group in test_groups: + for test in group: + if options.tests and test.name not in options.tests: + continue + try: + if rw_build_tree is None: + try: + group_options = group.check() + except Skip: + pass + else: + if group_options.rw_build_tree_needed: + rw_build_tree = copy_build_tree() + assert rw_build_tree is not None + group.run( + test, + progress=progress, + ignored_restrictions=options.ignore_restrictions, + rw_build_tree=rw_build_tree, + built_source_tree=options.built_source_tree, + ) + except Skip: + n_skip += 1 + except Fail as exc: + failures += [(test, exc)] + except Flaky as exc: + flakes += [(test, exc)] + else: + n_ok += 1 + finally: + progress.close() + n_fail = len(failures) + print(test_summary(failures, flakes, n_skip, n_ok)) + if rw_build_tree is not None: + shutil.rmtree(rw_build_tree) + sys.exit(n_fail > 0) + + +def main() -> None: + options = parse_args() + binary_packages = set() + build_depends = [] + try: + file = open("debian/control", encoding="UTF-8") + except IOError as exc: + if exc.errno == errno.ENOENT: + print("sadt: error: cannot find debian/control", file=sys.stderr) + sys.exit(1) + raise + with file: + for i, para in enumerate(deb822.Packages.iter_paragraphs(file)): + if i == 0: + # FIXME statement with no effect + # para['Source'] + for field in ( + "Build-Depends", + "Build-Depends-Indep", + "Build-Depends-Arch", + ): + try: + build_depends += parse_relations(para[field]) + except KeyError: + continue + else: + if para.get("Package-Type") == "udeb": + # udebs can't be tested + continue + binary_packages.add(para["Package"]) + + test_groups = get_test_groups(binary_packages, build_depends, options.run_autodep8) + run_tests(test_groups, options) + + +if __name__ == "__main__": + main() + +# vim:ts=4 sw=4 et diff --git a/scripts/sadt.pod b/scripts/sadt.pod new file mode 100644 index 0000000..3fd9d6d --- /dev/null +++ b/scripts/sadt.pod @@ -0,0 +1,76 @@ +# Copyright © 2013 Jakub Wilk <jwilk@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. + +=encoding utf8 + +=head1 NAME + +sadt - simple DEP-8 test runner + +=head1 SYNOPSIS + +B<sadt> [I<options>] [I<test-name>...] + +=head1 DESCRIPTION + +B<sadt> is a simple implementation of DEP-8 (“automatic as-installed package +testing”) test runner. + +It is your responsibility to satisfy tests' dependencies. B<sadt> won't +attempt to install any missing packages. If a test's dependencies cannot be +satisfied by packages that are currently installed, the test will be skipped. + +B<sadt> won't build the package even if a test declares the B<build-needed> +restriction. Instead, such a test will be skipped. However, you can build the +package manually, and then tell B<sadt> to assume that the package is already +built using the B<-b>/B<--built-source-tree>. + +B<sadt> doesn't implement any virtualisation arrangements, therefore it skips +tests that declare the B<breaks-testbed> restriction. + +=head1 OPTIONS + +=over 4 + +=item B<-v>, B<--verbose> + +Make the output more verbose. + +=item B<-b>, B<--built-source-tree> + +Assume that the source tree is already built. +This is equivalent to B<--ignore-restriction=build-needed>. + +=item B<--run-autodep8>, B<--no-run-autodep8> + +Control whether to run autodep8(1) to determine the tests to run. +By default, autodep8 will be run. + +=item B<--ignore-restriction>=I<restriction> + +Don't skip tests that declare the I<restriction>. + +=item B<-h>, B<--help> + +Show a help message and exit. + +=back + +=head1 CONFORMING TO + +README.package-tests shipped by autopkgtest 2.7.2 + +=head1 SEE ALSO + +B<adt-run>(1) diff --git a/scripts/salsa.bash_completion b/scripts/salsa.bash_completion new file mode 100644 index 0000000..29a2c95 --- /dev/null +++ b/scripts/salsa.bash_completion @@ -0,0 +1,109 @@ +# /usr/share/bash-completion/completions/salsa +# Bash command completion for ‘salsa(1)’. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +shopt -s progcomp + +_salsa_completion () { + COMPREPLY=() + + local cur="${COMP_WORDS[COMP_CWORD]}" + local prev="${COMP_WORDS[COMP_CWORD-1]}" + + local opts="" +# Source: ./lib/Devscripts/Config.pm:sub parse_command_line + opts+=" --help" +# Source: ./lib/Devscripts/Output.pm:sub ds_prompt + opts+=" --info" +# Source: ./lib/Devscripts/Config.pm:[...]$ARGV[0] + opts+=" --conf-file --no-conf" +# Source: ./lib/Devscripts/Salsa/Config.pm:use constant keys +# Note: '<VALUE>!' == '--<VALUE> --no-<VALUE>', '<SHORT>|<LONG>' +# Headings: $ grep '^ #' ./lib/Devscripts/Salsa/Config.pm + # General salsa + opts+=" --chdir --cache-file --no-cache --path" + # Responses + opts+=" --yes --no-yes --no-fail" + # Output + opts+=" --verbose --no-verbose --debug --info" + # General GitLab + opts+=" --user --user-id --group --group-id --token --token-file" + # List/search + opts+=" --all --all-archived --archived --no-archived" + opts+=" --skip --skip-file --no-skip" + # Features + opts+=" --analytics --auto-devops --container --environments" + opts+=" --feature-flags --forks --infrastructure --issues --jobs --lfs" + opts+=" --monitor --mr --packages --pages --releases --repo --service-desk" + opts+=" --request-access --requirements --security-compliance " + opts+=" --snippets --wiki" + # Branding + opts+=" --avatar-path --desc --no-desc --desc-pattern" + # Notification + opts+=" --email --no-email --disable-email --no-disable-email" + opts+=" --email-recipient --irc-channel --irker --no-irker --disable-irker" + opts+=" --no-disable-irker --irker-host --irker-port --kgb --no-kgb" + opts+=" --disable-kgb --no-disable-kgb --kgb-options --tagpending" + opts+=" --no-tagpending --disable-tagpending --no-disable-tagpending" + # Branch + opts+=" --rename-head --no-rename-head --source-branch --dest-branch" + opts+=" --enable-remove-source-branch --no-enable-remove-source-branch" + opts+=" --disable-remove-source-branch --no-disable-remove-source-branch" + # Merge requests + opts+=" --mr-allow-squash --no-mr-allow-squash --mr-desc --mr-dst-branch" + opts+=" --mr-dst-project --mr-remove-source-branch" + opts+=" --no-mr-remove-source-branch --mr-src-branch --mr-src-project" + opts+=" --mr-title" + # CI + opts+=" --build-timeout --ci-config-path" + # Pipeline schedules + opts+=" --schedule-desc --schedule-ref --schedule-cron --schedule-tz" + opts+=" --schedule-enable --no-schedule-enable --schedule-disable" + opts+=" --no-schedule-disable --schedule-run --no-schedule-run" + opts+=" --schedule-delete --no-schedule-delete" + # Manage other GitLab instances + opts+=" --api-url --git-server-url --irker-server-url --kgb-server-url" + opts+=" --tagpending-server-url + +# Source: ./lib/Devscripts/Salsa.pm:sub run -> $ ls ./lib/Devscripts/Salsa/*.pm +# Skipping: Config Hooks Repo -> `with "Devscripts::Salsa::<VALUE>";` +# Then filter from: ./lib/Devscripts/Salsa.pm:use constant cmd_aliases -> Preferred terminology + local commands="" +# Headings: $ grep '^=head' ./scripts/salsa.pl + # Managing users and groups + commands+=" add_user join list_groups update_user whoami" + # Managing projects + commands+=" checkout fork forks last_ci_status merge_request" + commands+=" merge_requests pipeline_schedule pipeline_schedules" + commands+=" protect_branch protected_branches push push_repo" + commands+=" rename_branch update_safe" + # Other + commands+=" purge_cache" + +# Aliases source: ./lib/Devscripts/Salsa.pm:use constant cmd_aliases -> Preferred terminology + commands+=" check_projects create_project delete_project delete_user" + commands+=" list_projects list_users search_groups search_projects " + commands+=" search_users update_projects" + + # Disable completion for the arguments which require variables afterwards + case "${prev}" in + --api-url) ;& + --desc-pattern) ;& + --irc-channel) ;& + --path) ;& + --group-id) ;& + --user-id) + COMPREPLY=() + ;; + *) + if [[ "$cur" == -* ]]; then + COMPREPLY=( $( compgen -W "$opts" -- $cur ) ) + else + COMPREPLY=( $( compgen -W "$commands" -- $cur ) ) + fi + ;; + esac + return 0 +} + +complete -F _salsa_completion salsa diff --git a/scripts/salsa.pl b/scripts/salsa.pl new file mode 100755 index 0000000..414c919 --- /dev/null +++ b/scripts/salsa.pl @@ -0,0 +1,1125 @@ +#!/usr/bin/perl + +=head1 NAME + +salsa - tool to manipulate salsa projects, repositories and group members + +=head1 SYNOPSIS + + # salsa <command> <parameters> <options> + salsa add_user developer foobar --group-id 2665 + salsa delete_user foobar --group js-team + salsa search_groups perl-team/modules + salsa search_projects qa/qa + salsa search_users yadd + salsa update_user maintainer foobar --group js-team + salsa whoami + salsa checkout node-mongodb --group js-team + salsa fork salsa fork --group js-team user/node-foo + salsa last_ci_status js-team/nodejs + salsa pipelines js-team/nodejs + salsa mr debian/foo debian/master + salsa push_repo . --group js-team --kgb --irc devscripts --tagpending + salsa update_projects node-mongodb --group js-team --disable-kgb --desc \ + --desc-pattern "Package %p" + salsa update_safe --all --desc --desc-pattern "Debian package %p" \ + --group js-team + +=head1 DESCRIPTION + +B<salsa> is designed to create and configure projects and repositories on +L<https://salsa.debian.org> as well as to manage group members. + +A Salsa token is required, except for search* commands, and must be set in +command line I<(see below)>, or in your configuration file I<(~/.devscripts)>: + + SALSA_TOKEN=abcdefghi + +or + + SALSA_TOKEN=`cat ~/.token` + +or + + SALSA_TOKEN_FILE=~/.dpt.conf + +If you choose to link another file using SALSA_TOKEN_FILE, it must contain a +line with one of (no differences): + + <anything>SALSA_PRIVATE_TOKEN=xxxx + <anything>SALSA_TOKEN=xxxx + +This allows for example to use dpt(1) configuration file (~/.dpt.conf) which +contains: + + DPT_SALSA_PRIVATE_TOKEN=abcdefghi + +=head1 COMMANDS + +=head2 Managing users and groups + +=over + +=item B<add_user> + +Add a user to a group. + + salsa --group js-group add_user guest foouser + salsa --group-id 1234 add_user guest foouser + salsa --group-id 1234 add_user maintainer 1245 + +First argument is the GitLab's access levels: guest, reporter, developer, +maintainer, owner. + +=item B<delete_user> or B<del_user> + +Remove a user from a group. + + salsa --group js-team delete_user foouser + salsa --group-id=1234 delete_user foouser + +=item B<join> + +Request access to a group. + + salsa join js-team + salsa join --group js-team + salsa join --group-id 1234 + +=item B<list_groups> + +List the subgroups for current group if group is set, otherwise +will do the current user. + +=item B<list_users> or B<group> + +List users in a subgroup. +Note, this does not include inherited or invited. + + salsa --group js-team list_users + salsa --group-id 1234 list_users + +=item B<search_groups> + +Search for a group using given string. Shows group ID and other +information. + + salsa search_groups perl-team + salsa search_groups perl-team/modules + salsa search_groups 2666 + +=item B<search_users> + +Search for a user using given string. Shows user ID and other information. + + salsa search_users yadd + +=item B<update_user> + +Update a user's role in a group. + + salsa --group-id 1234 update_user guest foouser + salsa --group js-team update_user maintainer 1245 + +First argument is the GitLab's access levels: guest, reporter, developer, +maintainer, owner. + +=item B<whoami> + +Gives information on the token owner. + + salsa whoami + +=back + +=head2 Managing projects + +One of C<--group>, C<--group-id>, C<--user> or C<--user-id> is required to +manage projects. If both are set, salsa warns and only +C<--user>/C<--user-id> is used. If none is given, salsa uses current user ID +I<(token owner)>. + +=over + +=item B<check_projects> or B<check_repo> + +Verify that projects are configured as expected. It works exactly like B<update_projects> +except that it does not modify anything but just lists projects not well +configured with found errors. + + salsa --user yadd --tagpending --kgb --irc=devscripts check_projects test + salsa --group js-team check_projects --all + salsa --group js-team --rename-head check_projects test1 test2 test3 + +=item B<checkout> or B<co> + +Clone a project's repository in current directory. If the directory already +exists, update local repository. + + salsa --user yadd checkout devscripts + salsa --group js-team checkout node-mongodb + salsa checkout js-team/node-mongodb + +You can clone more than one repository or all repositories of a group or a +user: + + salsa --user yadd checkout devscripts autodep8 + salsa checkout yadd/devscripts js-team/npm + salsa --group js-team checkout --all # All js-team active repositories + salsa checkout --all-archived # All your repositories, including archived + +=item B<create_project> or B<create_repo> + +Create public empty project. If C<--group>/C<--group-id> is set, project is +created in group directory, else in user directory. + + salsa --user yadd create_project test + salsa --group js-team --kgb --irc-channel=devscripts create_project test + +=item B<delete_project> or B<del_repo> + +Delete a project. + +=item B<fork> + +Forks a project in group/user repository and set "upstream" to original +project. Example: + + $ salsa fork js-team/node-mongodb --verbose + ... + salsa.pl info: node-mongodb ready in node-mongodb/ + $ cd node-mongodb + $ git remote --verbose show + origin git@salsa.debian.org:me/node-mongodb (fetch) + origin git@salsa.debian.org:me/node-mongodb (push) + upstream git@salsa.debian.org:js-team/node-mongodb (fetch) + upstream git@salsa.debian.org:js-team/node-mongodb (push) + +For a group: + + salsa fork --group js-team user/node-foo + +=item B<forks> + +List forks of project(s). + + salsa forks qa/qa debian/devscripts + +Project can be set using full path or using B<--group>/B<--group-id> or +B<--user>/B<--user-id>, else it is searched in current user namespace. + +=item B<push> + +Push relevant packaging refs to origin Git remote. To be run from packaging +working directory. + + salsa push + +It pushes the following refs to the configured remote for the debian-branch or, +falling back, to the "origin" remote: + +=over + +=item "master" branch (or whatever is set to debian-branch in gbp.conf) + +=item "upstream" branch (or whatever is set to upstream-branch in gbp.conf) + +=item "pristine-tar" branch + +=item tags named "debian/*" (or whatever is set to debian-tag in gbp.conf) + +=item tags named "upstream/*" (or whatever is set to upstream-tag in gbp.conf) + +=item all tags, if the package's source format is "3.0 (native)" + +=back + +=item B<list_projects> or B<list_repos> or B<ls> + +Shows projects owned by user or group. If second +argument exists, search only matching projects. + + salsa --group js-team list_projects + salsa --user yadd list_projects foo* + +=item B<last_ci_status> + +Displays the last continuous integration result. Use B<--verbose> to see +URL of pipeline when result isn't B<success>. Unless B<--no-fail> is set, +B<salsa last_ci_status> will stop on first "failed" status. + + salsa --group js-team last_ci_status --all --no-fail + salsa --user yadd last_ci_status foo + salsa last_ci_status js-team/nodejs + +This commands returns the number of "failed" status found. "success" entries +are displayed using STDOUT while other are displayed I<(with details)> using +STDERR. Then you can easily see only failures using: + + salsa --group js-team last_ci_status --all --no-fail >/dev/null + +=item B<pipeline_schedule> or B<schedule> + +Control pipeline schedule. + +=item B<pipeline_schedules> or B<schedules> + +Lists current pipeline schedule items. + +You can use B<--no-fail> and B<--all> options here. + +=item B<merge_request> or B<mr> + +Creates a merge request. + +Suppose you created a fork using B<salsa fork>, modify some things in a new +branch using one commit and want to propose it to original project +I<(branch "master")>. You just have to launch this in source directory: + + salsa merge_request + +Another example: + + salsa merge_request --mr-dst-project debian/foo --mr-dst-branch debian/master + +Or simply: + + salsa merge_request debian/foo debian/master + +Note that unless destination project has been set using command line, +B<salsa merge_request> will search it in the following order: + +=over 4 + +=item using GitLab API: salsa will detect from where this project was forked + +=item using "upstream" origin + +=item else salsa will use source project as destination project + +=back + +To force salsa to use source project as destination project, you can use +"same": + + salsa merge_request --mr-dst-project same + # or + salsa merge_request same + +New merge request will be created using last commit title and description. + +See B<--mr-*> options for more. + +=item B<merge_requests> or B<mrs> + +List opened merge requests for project(s). + + salsa merge_requests qa/qa debian/devscripts + +Project can be set using full path or using B<--group>/B<--group-id> or +B<--user>/B<--user-id>, else it is searched in current user namespace. + +=item B<protect_branch> + +Protect/unprotect a branch. + +=over + +=item Protect + + # project branch merge push + salsa --group js-team protect_branch node-mongodb master m d + +"merge" and "push" can be one of: + +=over + +=item B<o>, B<owner>: owner only + +=item B<m>, B<maintainer>: B<o> + maintainers allowed + +=item B<d>, B<developer>: B<m> + developers allowed + +=item B<r>, B<reporter>: B<d> + reporters allowed + +=item B<g>, B<guest>: B<r> + guest allowed + +=back + +=item Unprotect + + salsa --group js-team protect_branch node-mongodb master no + +=back + +=item B<protected_branches> + +List protected branches: + + salsa --group js-team protected_branches node-mongodb + +=item B<push_repo> + +Create a new project from a local Debian source directory configured with +git. + +B<push_repo> executes the following steps: + +=over + +=item gets project name using debian/changelog file; + +=item launches B<git remote add upstream ...>; + +=item launches B<create_project>; + +=item pushes local repository. + +=back + +Examples: + + salsa --user yadd push_repo ./test + salsa --group js-team --kgb --irc-channel=devscripts push_repo . + +=item B<rename_branch> + +Rename branch given in B<--source-branch> with name given in B<--dest-branch>. +You can use B<--no-fail>, B<--all> and B<--all-archived> options here. + +=item B<search_projects> or B<search_repo> or B<search> + +Search for a project using given string. Shows name, owner ID and other +information. + + salsa search_projects devscripts + salsa search_projects debian/devscripts + salsa search_projects 18475 + +=item B<update_projects> or B<update_repo> + +Configure projects using parameters given to command line. +A project name has to be given unless B<--all> or B<--all-archived> is set. Prefer to use +B<update_safe>. + + salsa --user yadd --tagpending --kgb --irc=devscripts update_projects test + salsa --group js-team update_projects --all + salsa --group js-team --rename-head update_projects test1 test2 test3 + salsa update_projects js-team/node-mongodb --kgb --irc debian-js + +By default when using B<--all>, salsa will fail on first error. If you want +to continue, set B<--no-fail>. In this case, salsa will display a warning for +each project that has fail but continue with next project. Then to see full +errors, set B<--verbose>. + +=item B<update_safe> + +Launch B<check_projects> and ask before launching B<update_projects> (unless B<--yes>). + + salsa --user yadd --tagpending --kgb --irc=devscripts update_safe test + salsa --group js-team update_safe --all + salsa --group js-team --rename-head update_safe test1 test2 test3 + salsa update_safe js-team/node-mongodb --kgb --irc debian-js + +=back + +=head2 Other + +=over + +=item B<purge_cache> + +Empty local cache. + +=back + +=head1 OPTIONS + +=head2 General options + +=over + +=item B<--chdir> or B<-C> + +Change directory before launching command: + + salsa --chdir ~/debian checkout debian/libapache2-mod-fcgid + +=item B<--cache-file> + +File to store cached values. An empty value disables cache. +Default: C<~/.cache/salsa.json>. + +C<.devscripts> value: B<SALSA_CACHE_FILE> + +=item B<--no-cache> + +Disable cache usage. Same as B<--cache-file ''> + +=item B<--conf-file> or B<--conffile> + +Add or replace default configuration files. +This can only be used as the first option given on the +command-line. +Default: C</etc/devscripts.conf> and C<~/.devscripts>. + +=over + +=item replace: + + salsa --conf-file test.conf <command>... + salsa --conf-file test.conf --conf-file test2.conf <command>... + +=item add: + + salsa --conf-file +test.conf <command>... + salsa --conf-file +test.conf --conf-file +test2.conf <command>... + +If one B<--conf-file> has no C<+>, default configuration files are ignored. + +=back + +=item B<--no-conf> or B<--noconf> + +Don't read any configuration files. This can only be used as the first option +given on the command-line. + +=item B<--debug> + +Enable debugging output. + +=item B<--group> + +Team to use. Use C<salsa search_groups name> to find it. + +If you want to use a subgroup, you have to set its full path: + + salsa --group perl-team/modules/packages check_projects lemonldap-ng + +C<.devscripts> value: B<SALSA_GROUP> + +Be careful when you use B<SALSA_GROUP> in your C<.devscripts> file. Every +B<salsa> command will be executed in group space, for example if you want to +propose a little change in a project using B<salsa fork> + B<salsa merge_request>, this +"fork" will be done in group space unless you set a B<--user>/B<--user-id>. +Prefer to use an alias in your C<.bashrc> file. Example: + + alias jsteam_admin="salsa --group js-team" + +or + + alias jsteam_admin="salsa --conf-file ~/.js.conf + +or to use both .devscripts and .js.conf: + + alias jsteam_admin="salsa --conf-file +~/.js.conf + +then you can fix B<SALSA_GROUP> in C<~/.js.conf> + +To enable bash completion for your alias, add this in your .bashrc file: + + _completion_loader salsa + complete -F _salsa_completion jsteam_admin + +=item B<--group-id> + +Group ID to use. Use C<salsa search_groups name> to find it. + +C<.devscripts> value: B<SALSA_GROUP_ID> + +Be careful when you use B<SALSA_GROUP_ID> in your C<.devscripts> file. Every +B<salsa> command will be executed in group space, for example if you want to +propose a little change in a project using B<salsa fork> + B<salsa merge_request>, this +"fork" will be done in group space unless you set a B<--user>/B<--user-id>. +Prefer to use an alias in your C<.bashrc> file. Example: + + alias jsteam_admin="salsa --group-id 2666" + +or + + alias jsteam_admin="salsa --conf-file ~/.js.conf + +then you can fix B<SALSA_GROUP_ID> in C<~/.js.conf>. + +=item B<--help> + +Displays this manpage. + +=item B<--info> or B<-i> + +Prompt before sensible changes. + +C<.devscripts> value: B<SALSA_INFO> (yes/no) + +=item B<--path> + +Repository path. +Default to group or user path. + +C<.devscripts> value: B<SALSA_REPO_PATH> + +=item B<--token> + +Token value (see above). + +=item B<--token-file> + +File to find token (see above). + +=item B<--user> + +Username to use. If neither B<--group>, B<--group-id>, B<--user> or B<--user-id> +is set, salsa uses current user ID (corresponding to salsa private token). + +=item B<--user-id> + +User ID to use. Use C<salsa search_users name> to find one. If neither +B<--group>, B<--group-id>, B<--user> or B<--user-id> is set, salsa uses current +user ID (corresponding to salsa private token). + +C<.devscripts> value: B<SALSA_USER_ID> + +=item B<--verbose> + +Enable verbose output. + +=item B<--yes> + +Never ask for consent. + +C<.devscripts> value: B<SALSA_YES> (yes/no) + +=back + +=head2 List/search project options + +=over + +=item B<--archived>, B<--no-archived> + +Instead of looking to active projects, list or search in archived projects. +Note that you can't have both archived and unarchived projects in the same +request. +Default: no I<(ie --no-archived)>. + +C<.devscripts> value: B<SALSA_ARCHIVED> (yes/no) + +=back + +=head2 Update/create project options + +=over + +=item B<--all>, B<--all-archived> + +When set, all projects of group/user are affected by command. +B<--all> will filter all active projects, whereas B<--all-archived> will +include active and archived projects. + +=over + +=item B<--skip>, B<--no-skip> + +Ignore project with B<--all> or B<--all-achived>. Example: + + salsa update_projects --tagpending --all --skip qa --skip devscripts + +To set multiples values, use spaces. Example: + + SALSA_SKIP=qa devscripts + +Using B<--no-skip> will ignore any projects to be skipped and include them. + +C<.devscripts> value: B<SALSA_SKIP> + +=item B<--skip-file> + +Ignore projects in this file (1 project per line). + + salsa update_projects --tagpending --all --skip-file ~/.skip + +C<.devscripts> value: B<SALSA_SKIP_FILE> + +=back + +=item B<--build-timeout> + +The maximum amount of time, in seconds, that a job can run. +Default: 3600 (60 minutes). + + salsa update_safe myrepo --build-timeout 3600 + +C<.devscripts> value: B<SALSA_BUILD_TIMEOUT> + +=item B<--avatar-path> + +Path to an image for the project's avatar. +If path value contains "%p", it is replaced by project name. + +C<.devscripts> value: B<SALSA_AVATAR_PATH> + +=item B<--ci-config-path> + +Configure configuration file path of GitLab CI. +Default: empty. +Example: + + salsa update_safe --ci-config-path recipes/debian.yml@salsa-ci-team/pipeline debian/devscripts + +C<.devscripts> value: B<SALSA_CI_CONFIG_PATH> + +=item B<--desc>, B<--no-desc> + +Configure a project's description using pattern given in B<desc-pattern>. + +C<.devscripts> value: B<SALSA_DESC> (yes/no) + +=item B<--desc-pattern> + +Project's description pattern. "%p" is replaced by project's name, +while "%P" is replaced by project's name given in command +(may contains full path). +Default: "Debian package %p". + +C<.devscripts> value: B<SALSA_DESC_PATTERN> + +=item B<--email>, B<--no-email>, B<--disable-email> + +Enable, ignore or disable email-on-push. + +C<.devscripts> value: B<SALSA_EMAIL> (yes/ignore/no, default: ignore) + +=item B<--email-recipient> + +Email-on-push recipient. Can be multi valued: + + $ salsa update_safe myrepo \ + --email-recipient foo@foobar.org \ + --email-recipient bar@foobar.org + +If recipient value contains "%p", it is replaced by project name. + +C<.devscripts> value: B<SALSA_EMAIL_RECIPIENTS> (use spaces to separate +multiples recipients) + +=item B<--analytics> + +Set analytics feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_ANALYTICS> (yes/private/no, default: yes) + +=item B<--auto-devops> + +Set auto devops feature. + +C<.devscripts> value: B<SALSA_ENABLE_AUTO_DEVOPS> (yes/no, default: yes) + +=item B<--container> + +Set container feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_CONTAINER> (yes/private/no, default: yes) + +=item B<--environments> + +Set environments feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_ENVIRONMENTS> (yes/private/no, default: yes) + +=item B<--feature-flags> + +Set feature flags feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_FEATURE_FLAGS> (yes/private/no, default: yes) + +=item B<--forks> + +Set forking a project feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_FORKS> (yes/private/no, default: yes) + +=item B<--infrastructure> + +Set infrastructure feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_INFRASTRUCTURE> (yes/private/no, default: yes) + +=item B<--issues> + +Set issues feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_ISSUES> (yes/private/no, default: yes) + +=item B<--jobs> + +Set jobs feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_JOBS> (yes/private/no, default: yes) + +=item B<--lfs> + +Set Large File Storage (LFS) feature. + +C<.devscripts> value: B<SALSA_ENABLE_LFS> (yes/no, default: yes) + +=item B<--mr> + +Set merge requests feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_MR> (yes/private/no, default: yes) + +=item B<--monitor> + +Set monitor feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_MONITOR> (yes/private/no, default: yes) + +=item B<--packages> + +Set packages feature. + +C<.devscripts> value: B<SALSA_ENABLE_PACKAGES> (yes/no, default: yes) + +=item B<--pages> + +Set pages feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_PAGES> (yes/private/no, default: yes) + +=item B<--releases> + +Set releases feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_RELEASES> (yes/private/no, default: yes) + +=item B<--enable-remove-source-branch>, B<--disable-remove-source-branch> + +Enable or disable deleting source branch option by default for all new merge +requests. + +C<.devscripts> value: B<SALSA_REMOVE_SOURCE_BRANCH> (yes/no, default: yes) + +=item B<--repo> + +Set the project's repository feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_REPO> (yes/private/no, default: yes) + +=item B<--request-access> + +Allow users to request member access. + +C<.devscripts> value: B<SALSA_REQUEST_ACCESS> (yes/no) + +=item B<--requirements> + +Set requirements feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_REQUIREMENTS> (yes/private/no, default: yes) + +=item B<--security-compliance> + +Enable or disabled Security and Compliance feature. + +C<.devscripts> value: B<SALSA_ENABLE_SECURITY_COMPLIANCE> (yes/no) + +=item B<--service-desk> + +Allow service desk feature. + +C<.devscripts> value: B<SALSA_ENABLE_SERVICE_DESK> (yes/no) + +=item B<--snippets> + +Set snippets feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_SNIPPETS> (yes/private/no, default: yes) + +=item B<--wiki> + +Set wiki feature with permissions. + +C<.devscripts> value: B<SALSA_ENABLE_WIKI> (yes/private/no, default: yes) + +=item B<--irc-channel> + +IRC channel for KGB or Irker. Can be used more than one time only with +B<--irker>. + +B<Important>: channel must not include the first "#". If salsa finds a channel +starting with "#", it will consider that the channel starts with 2 "#"! + +C<.devscript> value: B<SALSA_IRC_CHANNEL> + +Multiple values must be space separated. + +Since configuration files are read using B<sh>, be careful when using "#": you +must enclose the channel with quotes, else B<sh> will consider it as a comment +and will ignore this value. + +=item B<--irker>, B<--no-irker>, B<--disable-irker> + +Enable, ignore or disable Irker service. + +C<.devscripts> value: B<SALSA_IRKER> (yes/ignore/no, default: ignore) + +=item B<--irker-host> + +Irker host. +Default: ruprecht.snow-crash.org. + +C<.devscripts> value: B<SALSA_IRKER_HOST> + +=item B<--irker-port> + +Irker port. +Default: empty (default value). + +C<.devscripts> value: B<SALSA_IRKER_PORT> + +=item B<--kgb>, B<--no-kgb>, B<--disable-kgb> + +Enable, ignore or disable KGB webhook. + +C<.devscripts> value: B<SALSA_KGB> (yes/ignore/no, default: ignore) + +=item B<--kgb-options> + +List of KGB enabled options (comma separated). +Default: issues_events, merge_requests_events, note_events, +pipeline_events, push_events, tag_push_events, wiki_page_events, +enable_ssl_verification + + $ salsa update_safe debian/devscripts --kgb --irc-channel devscripts \ + --kgb-options 'merge_requests_events,issues_events,enable_ssl_verification' + +List of available options: confidential_comments_events, +confidential_issues_events, confidential_note_events, enable_ssl_verification, +issues_events, job_events, merge_requests_events, note_events, pipeline_events, +tag_push_events, wiki_page_events + +C<.devscripts> value: B<SALSA_KGB_OPTIONS> + +=item B<--no-fail> + +Don't stop on error when using B<update_projects> with B<--all> or B<--all-archived> +when set to yes. + +C<.devscripts> value: B<SALSA_NO_FAIL> (yes/no, default: no) + +=item B<--rename-head>, B<--no-rename-head> + +Rename HEAD branch given by B<--source-branch> into B<--dest-branch> and change +"default branch" of project. Works only with B<update_projects>. + +C<.devscripts> value: B<SALSA_RENAME_HEAD> (yes/no) + +=over + +=item B<--source-branch> + +Default: "master". + +C<.devscripts> value: B<SALSA_SOURCE_BRANCH> + +=item B<--dest-branch> + +Default: "debian/master". + +C<.devscripts> value: B<SALSA_DEST_BRANCH> + +=back + +=item B<--tagpending>, B<--no-tagpending>, B<--disable-tagpending> + +Enable, ignore or disable "tagpending" webhook. + +C<.devscripts> value: B<SALSA_TAGPENDING> (yes/ignore/no, default: ignore) + +=back + +=head2 Pipeline schedules + +=over + +=item B<--schedule-desc> + +Description of the pipeline schedule. + +=item B<--schedule-ref> + +Branch or tag name that is triggered. + +=item B<--schedule-cron> + +Cron schedule. Example: + + 0 1 * * *. + +=item B<--schedule-tz> + +Time zone to run cron schedule. +Default: UTC. + +=item B<--schedule-enable>, B<--schedule-disable> + +Enable/disable the pipeline schedule to run. +Default: disabled. + +=item B<--schedule-run> + +Trigger B<--schedule-desc> scheduled pipeline to run immediately. +Default: false. + +=item B<--schedule-delete> + +Delete B<--schedule-desc> pipeline schedule. + +=back + +=head2 Merge requests options + +=over + +=item B<--mr-title> + +Title for merge request. +Default: last commit title. + +=item B<--mr-desc> + +Description of new MR. +Default: + +=over + +=item empty if B<--mr-title> is set + +=item last commit description if any + +=back + +=item B<--mr-dst-branch> (or second command line argument) + +Destination branch. +Default: "master". + +=item B<--mr-dst-project> (or first command line argument) + +Destination project. +Default: project from which the current project was forked; or, +if not found, "upstream" value found using B<git remote --verbose show>; +or using source project. + +If B<--mr-dst-project> is set to B<same>, salsa will use source project as +destination. + +=item B<--mr-src-branch> + +Source branch. +Default: current branch. + +=item B<--mr-src-project> + +Source project. +Default: current project found using +B<git remote --verbose show>. + +=item B<--mr-allow-squash>, B<--no-mr-allow-squash> + +Allow upstream project to squash your commits, this is the default. + +C<.devscripts> value: B<SALSA_MR_ALLOW_SQUASH> (yes/no) + +=item B<--mr-remove-source-branch>, B<--no-mr-remove-source-branch> + +Remove source branch if merge request is accepted. +Default: no. + +C<.devscripts> value: B<SALSA_MR_REMOVE_SOURCE_BRANCH> (yes/no) + +=back + +=head2 Options to manage other GitLab instances + +=over + +=item B<--api-url> + +GitLab API. +Default: L<https://salsa.debian.org/api/v4>. + +C<.devscripts> value: B<SALSA_API_URL> + +=item B<--git-server-url> + +Default: "git@salsa.debian.org:". + +C<.devscripts> value: B<SALSA_GIT_SERVER_URL> + +=item B<--irker-server-url> + +Default: "ircs://irc.oftc.net:6697/". + +C<.devscripts> value: B<SALSA_IRKER_SERVER_URL> + +=item B<--kgb-server-url> + +Default: L<https://kgb.debian.net/webhook/?channel=>. + +C<.devscripts> value: B<SALSA_KGB_SERVER_URL> + +=item B<--tagpending-server-url> + +Default: L<https://webhook.salsa.debian.org/tagpending/>. + +C<.devscripts> value: B<SALSA_TAGPENDING_SERVER_URL> + +=back + +=head3 Configuration file example + +Example to use salsa with L<https://gitlab.ow2.org> (group "lemonldap-ng"): + + SALSA_TOKEN=`cat ~/.ow2-gitlab-token` + SALSA_API_URL=https://gitlab.ow2.org/api/v4 + SALSA_GIT_SERVER_URL=git@gitlab.ow2.org: + SALSA_GROUP_ID=34 + +Then to use it, add something like this in your C<.bashrc> file: + + alias llng_admin='salsa --conffile ~/.salsa-ow2.conf' + +=head1 SEE ALSO + +B<dpt-salsa> + +=head1 AUTHOR + +Xavier Guimard E<lt>yadd@debian.orgE<gt> + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2018, Xavier Guimard E<lt>yadd@debian.orgE<gt> + +It contains code formerly found in L<dpt-salsa> I<(pkg-perl-tools)> +copyright 2018, gregor herrmann E<lt>gregoa@debian.orgE<gt>. + +This library 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 L<http://www.gnu.org/licenses/>. + +=cut + +use Devscripts::Salsa; + +exit Devscripts::Salsa->new->run; diff --git a/scripts/setup.py b/scripts/setup.py new file mode 100755 index 0000000..67caad5 --- /dev/null +++ b/scripts/setup.py @@ -0,0 +1,52 @@ +#!/usr/bin/python3 + +import pathlib +import re + +from setuptools import setup + +from devscripts.test import SCRIPTS + + +def get_debian_version() -> str: + """Determine the Debian package version from debian/changelog.""" + changelog = pathlib.Path(__file__).parent.parent / "debian" / "changelog" + with changelog.open(encoding="utf8") as f: + head = f.readline() + match = re.match(r".*\((.*)\).*", head) + assert match, f"Failed to extract version from '{head}'." + return match.group(1) + + +def make_pep440_compliant(version: str) -> str: + """Convert the version into a PEP440 compliant version.""" + public_version_re = re.compile( + r"^([0-9][0-9.]*(?:(?:a|b|rc|.post|.dev)[0-9]+)*)\+?" + ) + _, public, local = public_version_re.split(version, maxsplit=1) + if not local: + return version + sanitized_local = re.sub("[+~]+", ".", local).strip(".") + pep440_version = f"{public}+{sanitized_local}" + assert re.match( + "^[a-zA-Z0-9.]+$", sanitized_local + ), f"'{pep440_version}' not PEP440 compliant" + return pep440_version + + +def write_version(version: str) -> None: + """Write version into devscripts/__init__.py.""" + init_py = pathlib.Path(__file__).parent / "devscripts" / "__init__.py" + init_py.write_text(f'__version__ = "{version}"\n', encoding="utf-8") + + +if __name__ == "__main__": + VERSION = make_pep440_compliant(get_debian_version()) + write_version(VERSION) + setup( + name="devscripts", + version=VERSION, + scripts=SCRIPTS, + packages=["devscripts"], + test_suite="devscripts.test", + ) diff --git a/scripts/suspicious-source b/scripts/suspicious-source new file mode 100755 index 0000000..24cdf1c --- /dev/null +++ b/scripts/suspicious-source @@ -0,0 +1,177 @@ +#!/usr/bin/python3 + +# Copyright (c) 2010-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. + +# pylint: disable=invalid-name +# pylint: enable=invalid-name + +import argparse +import os +import sys + +from devscripts.logger import Logger + +try: + import magic +except ImportError: + Logger.error("Please install 'python3-magic' in order to use this utility.") + sys.exit(1) + +DEFAULT_WHITELISTED_MIMETYPES = [ + "application/pgp-keys", + "application/vnd.font-fontforge-sfd", # font source: fontforge + "application/x-elc", + "application/x-empty", + "application/x-font-otf", # font object and source + "application/x-font-ttf", # font object and source + "application/x-font-woff", # font object and source + "application/x-symlink", + "application/xml", + "audio/x-wav", + "font/otf", # font object and source + "font/ttf", # font object and source + "image/gif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/vnd.adobe.photoshop", + "image/x-icns", + "image/x-ico", + "image/x-icon", + "image/x-ms-bmp", + "image/x-portable-pixmap", + "image/x-xpmi", + "inode/symlink", + "inode/x-empty", + "message/rfc822", + "text/html", + "text/plain", + "text/rtf", + "text/troff", + "text/x-asm", + "text/x-c", + "text/x-c++", + "text/x-diff", + "text/x-fortran", + "text/x-java", + "text/x-lisp", + "text/x-m4", + "text/x-makefile", + "text/x-msdos-batch", + "text/x-pascal", + "text/x-perl", + "text/x-php", + "text/x-po", + "text/x-ruby", + "text/x-script.python", + "text/x-shellscript", + "text/x-tex", + "text/x-texinfo", + "text/xml", +] + +DEFAULT_WHITELISTED_EXTENSIONS = [ + ".el", # elisp source files + ".fea", # font source format: Adobe Font Development Kit for OpenType + ".fog", # font source format: Fontographer + ".g2n", # font source format: fontforge + ".gdh", # font source format: Graphite (headers) + ".gdl", # font source format: Graphite + ".glyph", # font source format: cross-toolkit UFO + ".gmo", # GNU Machine Object File (for translations with gettext) + ".icns", # Apple Icon Image format + ".java", # Java source files + ".plate", # font source format: Spiro + ".rsa", + ".sfd", # font source format: fontforge + ".sfdir", # font source format: fontforge + ".ttx", # font source format: fonttools + ".ufo", # font source format: cross-toolkit UFO + ".vfb", # font source format: FontLab + ".vtp", # font source format: OpenType (VOLT) + ".xgf", # font source format: Xgridfit +] + + +def suspicious_source( + whitelisted_mimetypes, whitelisted_extensions, directory, verbose=False +): + magic_cookie = magic.open(magic.MAGIC_MIME_TYPE) + magic_cookie.load() + + for root, dirs, files in os.walk(directory): + for _file in files: + mimetype = magic_cookie.file(os.path.join(root, _file)) + if mimetype not in whitelisted_mimetypes: + if not [x for x in whitelisted_extensions if _file.lower().endswith(x)]: + output = os.path.join(root, _file) + if verbose: + output += " (" + mimetype + ")" + print(output) + for vcs_dir in (".bzr", "CVS", ".git", ".svn", ".hg", "_darcs"): + if vcs_dir in dirs: + dirs.remove(vcs_dir) + + +def main(): + script_name = os.path.basename(sys.argv[0]) + epilog = f"See {script_name}(1) for more info." + parser = argparse.ArgumentParser(epilog=epilog) + + parser.add_argument( + "-v", + "--verbose", + help="print more information", + dest="verbose", + action="store_true", + default=False, + ) + parser.add_argument( + "-d", + "--directory", + help="check the files in the specified directory", + dest="directory", + default=".", + ) + parser.add_argument( + "-m", + "--mimetype", + metavar="MIMETYPE", + help="Add MIMETYPE to list of whitelisted mimetypes.", + dest="whitelisted_mimetypes", + action="append", + default=DEFAULT_WHITELISTED_MIMETYPES, + ) + parser.add_argument( + "-e", + "--extension", + metavar="EXTENSION", + help="Add EXTENSION to list of whitelisted extensions.", + dest="whitelisted_extensions", + action="append", + default=DEFAULT_WHITELISTED_EXTENSIONS, + ) + + args = parser.parse_args() + + whitelisted_extensions = [x.lower() for x in args.whitelisted_extensions] + suspicious_source( + args.whitelisted_mimetypes, whitelisted_extensions, args.directory, args.verbose + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/svnpath.pl b/scripts/svnpath.pl new file mode 100755 index 0000000..f2fc8f9 --- /dev/null +++ b/scripts/svnpath.pl @@ -0,0 +1,100 @@ +#!/usr/bin/perl + +=head1 NAME + +svnpath - output svn url with support for tags and branches + +=head1 SYNOPSIS + +B<svnpath> + +B<svnpath tags> + +B<svnpath branches> + +B<svnpath trunk> + +=head1 DESCRIPTION + +B<svnpath> is intended to be run in a Subversion working copy. + +In its simplest usage, B<svnpath> with no parameters outputs the svn url for +the repository associated with the working copy. + +If a parameter is given, B<svnpath> attempts to instead output the url that +would be used for the tags, branches, or trunk. This will only work if it's +run in the top-level directory that is subject to tagging or branching. + +For example, if you want to tag what's checked into Subversion as version +1.0, you could use a command like this: + + svn cp $(svnpath) $(svnpath tags)/1.0 + +That's much easier than using svn info to look up the repository url and +manually modifying it to derive the url to use for the tag, and typing in +something like this: + + svn cp svn+ssh://my.server.example/svn/project/trunk svn+ssh://my.server.example/svn/project/tags/1.0 + +svnpath uses a simple heuristic to convert between the trunk, tags, and +branches paths. It replaces the first occurrence of B<trunk>, B<tags>, or +B<branches> with the name of what you're looking for. This will work ok for +most typical Subversion repository layouts. + +If you have an atypical layout and it does not work, you can add a +F<~/.svnpath> file. This file is perl code, which can modify the path in $url. +For example, the author uses this file: + + #!/usr/bin/perl + # svnpath personal override file + + # For d-i I sometimes work from a full d-i tree branch. Remove that from + # the path to get regular tags or branches directories. + $url=~s!d-i/(rc|beta)[0-9]+/!!; + $url=~s!d-i/sarge/!!; + 1 + +=cut + +use File::HomeDir; + +$ENV{LANG} = "C"; + +my $wanted = shift; +my $path = shift; + +if (length $path) { + chdir $path || die "$path: unreadable\n"; +} + +our $url = `svn info . 2>/dev/null|grep -i ^URL: | cut -d ' ' -f 2`; +if (!length $url) { + # Try svk instead. + $url = `svk info .| grep -i '^Depot Path:' | cut -d ' ' -f 3`; +} + +if (!length $url) { + die "cannot get url"; +} + +if (length $wanted) { + # Now jut substitute into it. + $url =~ s!/(?:trunk|branches|tags)($|/)!/$wanted$1!; + + my $svnpath = File::HomeDir->my_home . "/.svnpath"; + if (-e $svnpath) { + require $svnpath; + } +} + +print $url; + +=head1 LICENSE + +GPL version 2 or later + +=head1 AUTHOR + +Joey Hess <joey@kitenet.net> + +=cut diff --git a/scripts/tagpending.pl b/scripts/tagpending.pl new file mode 100755 index 0000000..910e580 --- /dev/null +++ b/scripts/tagpending.pl @@ -0,0 +1,437 @@ +#!/usr/bin/perl +# +# tagpending: Parse a Debian changelog for a list of bugs closed +# and tag any that are not already pending as such. +# +# The original shell version of tagpending was written by Joshua Kwan +# and is Copyright 2004 Joshua Kwan <joshk@triplehelix.org> +# with changes copyright 2004-07 by their respective authors. +# +# This version is +# Copyright 2008 Adam D. Barratt +# +# 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 strict; +use warnings; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use File::Basename; +use Dpkg::Changelog::Parse qw(changelog_parse); +use Devscripts::Debbugs; + +sub bugs_info; + +my $progname = basename($0); + +my ($opt_help, $opt_version, $opt_verbose, $opt_noact, $opt_silent); +my ($opt_online, $opt_confirm, %opt_to, $opt_wnpp, $opt_comments); +my $opt_interactive; + +# Default options +$opt_silent = 0; +$opt_verbose = 0; +$opt_online = 1; +$opt_noact = 0; +$opt_confirm = 0; +$opt_wnpp = 0; +%opt_to = (); +$opt_comments = 1; +$opt_interactive = 0; + +GetOptions( + "help|h" => \$opt_help, + "version" => \$opt_version, + "verbose|v!" => \$opt_verbose, + "noact|n" => \$opt_noact, + "comments!" => \$opt_comments, + "silent|s" => \$opt_silent, + "force|f" => sub { $opt_online = 0; }, + "confirm|c" => \$opt_confirm, + "to|t=s" => sub { $opt_to{'-v'} = $_[1] }, + "wnpp|w" => \$opt_wnpp, + "interactive|i" => \$opt_interactive, + ) + or die "Usage: $progname [options]\nRun $progname --help for more details\n"; + +if ($opt_help) { + help(); + exit 0; +} elsif ($opt_version) { + version(); + exit 0; +} + +if ($opt_verbose and $opt_silent) { + die "$progname error: --silent and --verbose contradict each other\n"; +} + +=head1 NAME + +tagpending - tags bugs that are to be closed in the latest changelog as pending + +=head1 SYNOPSIS + +B<tagpending> [I<options>] + +=head1 DESCRIPTION + +B<tagpending> parses debian/changelog to determine +which bugs would be closed if the package were uploaded. Each bug is +then marked as pending, using B<bts>(1) if it is not already so. + +=head1 OPTIONS + +=over 4 + +=item B<-n>, B<--noact> + +Check whether any bugs require tagging, but do not actually do so. + +=item B<-s>, B<--silent> + +Do not output any messages. + +=item B<-v>, B<--verbose> + +List each bug checked and tagged in turn. + +=item B<-f>, B<--force> + +Do not query the BTS, but (re)tag all bugs closed in the changelog. + +=item B<--comments> + +Include the changelog header line and the entries relating to the tagged +bugs as comments in the generated mail. This is the default. + +Note that when used in combination with B<--to>, the header line output +will always be that of the most recent version. + +=item B<--no-comments> + +Do not include changelog entries in the generated mail. + +=item B<-c>, B<--confirm> + +Tag bugs as both confirmed and pending. + +=item B<-t>, B<--to> I<version> + +Parse changelogs for all versions strictly greater than I<version>. + +Equivalent to B<dpkg-parsechangelog>'s B<-v> option. + +=item B<-i>, B<--interactive> + +Display the message which would be sent to the BTS and, except when +B<--noact> was used, prompt for confirmation before sending it. + +=item B<-w>, B<--wnpp> + +For each bug that does not appear to belong to the current package, +check whether it is filed against wnpp. If so, tag it. This allows e.g. +ITAs and ITPs closed in an upload to be tagged. + +=back + +=head1 SEE ALSO + +B<bts>(1) and B<dpkg-parsechangelog>(1) + +=cut + +if (!-f 'debian/changelog') { + die "$progname error: debian/changelog does not exist!\n"; +} + +my $changelog = changelog_parse(%opt_to); +my $source = $changelog->{Source}; +my @closes; +if ($changelog->{Closes}) { + @closes = split ' ', $changelog->{Closes}; +} +my @lines = split /\n/, $changelog->{Changes}; +my $header = $lines[1]; +my $changes = join "\n", grep /^ {3}[^[]/, @lines; + +# Add a fake entry to the end of the recorded changes +# This makes the parsing of the changes simpler +$changes .= " *"; + +my $pending; +my $open; +my %bugs = map { $_ => 1 } @closes; + +if (%bugs) { + if ($opt_online) { + if (!Devscripts::Debbugs::have_soap()) { + die +"$progname: The libsoap-lite-perl package is required for online operation; aborting.\n"; + } + + eval { + $pending = Devscripts::Debbugs::select( + "src:$source", "status:open", + "status:forwarded", "tag:pending" + ); + $open = Devscripts::Debbugs::select("src:$source", "status:open", + "status:forwarded"); + }; + + if ($@) { + die "$@\nUse --force to tag all bugs anyway.\n"; + } + } + + if ($pending) { + %bugs = (%bugs, map { $_ => 1 } @{$pending}); + } +} + +my $bug; +my $message; +my @to_tag = (); +my @wnpp_to_tag = (); + +foreach $bug (keys %bugs) { + print "Checking bug #$bug: " if $opt_verbose; + + if (grep /^$bug$/, @{$pending}) { + print "already marked pending\n" if $opt_verbose; + } else { + if (grep /^$bug$/, @{$open} or not $opt_online) { + print "needs tag\n" if $opt_verbose; + push(@to_tag, $bug); + } else { + if ($opt_wnpp) { + my $status = Devscripts::Debbugs::status($bug); + if ($status->{$bug}->{package} eq 'wnpp') { + if ($status->{$bug}->{tags} !~ /pending/) { + print "wnpp needs tag\n" if $opt_verbose; + push(@wnpp_to_tag, $bug); + } else { + print "wnpp already marked pending\n" if $opt_verbose; + } + } else { + $message + = "is closed or does not belong to this package (check bug # or force)\n"; + + print "Warning: #$bug " if not $opt_verbose; + print "$message"; + } + } else { + $message + = "is closed or does not belong to this package (check bug # or force)\n"; + + print "Warning: #$bug " if not $opt_verbose; + print "$message"; + } + } + } +} + +if (!@to_tag and !@wnpp_to_tag) { + print "$progname info: Nothing to do, exiting.\n" + if $opt_verbose or !$opt_silent; + exit 0; +} + +my @sourcepkgs = (); +my @thiscloses = (); +my $thischange = ''; +my $comments = ''; + +if (@to_tag or @wnpp_to_tag) { + if ($opt_comments) { + foreach my $change (split /\n/, $changes) { + if ($change =~ /^ {3}\*(.*)/) { + # Adapted from dpkg-parsechangelog / Changelog.pm + while ( + $thischange + && ($thischange + =~ /closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*/sig + ) + ) { + push(@thiscloses, $& =~ /\#?\s?(\d+)/g); + } + + foreach my $bug (@thiscloses) { + if ($bug and grep /^$bug$/, + @to_tag or grep /^$bug$/, @wnpp_to_tag) { + $comments .= $thischange; + last; + } + } + + @thiscloses = (); + $thischange = $change; + } else { + $thischange .= $change . "\n"; + } + } + + $comments = $header . "\n \n" . $comments . "\n \n" + if $comments; + } +} + +my @bts_args = ("bts", "--toolname", $progname); + +if ($opt_noact and not $opt_interactive) { + bugs_info; + bugs_info "wnpp" if $opt_wnpp; +} else { + if (!$opt_silent) { + bugs_info; + bugs_info "wnpp" if $opt_wnpp; + } + + if ($opt_interactive) { + if ($opt_noact) { + push(@bts_args, "-n"); + print "\nWould send this BTS mail:\n\n"; + } else { + push(@bts_args, "-i"); + } + } + + if (@to_tag) { + push(@bts_args, "limit", "source:$source"); + + if ($comments) { + $comments =~ s/\n\n/\n/sg; + $comments =~ s/\n\n/\n/m; + $comments =~ s/^ /#/mg; + push(@bts_args, $comments); + # We don't want to add comments twice if there are + # both package and wnpp bugs + $comments = ''; + } + + foreach my $bug (@to_tag) { + push(@bts_args, ".", "tag", $bug, "+", "pending"); + push(@bts_args, "confirmed") if $opt_confirm; + } + } + if (@wnpp_to_tag) { + push(@bts_args, ".") if scalar @bts_args > 1; + push(@bts_args, "package", "wnpp"); + + if ($comments) { + $comments =~ s/\n\n/\n/sg; + $comments =~ s/^ /#/mg; + push(@bts_args, $comments); + } + + foreach my $wnpp_bug (@wnpp_to_tag) { + push(@bts_args, ".", "tag", $wnpp_bug, "+", "pending"); + } + } + + system @bts_args; +} + +sub bugs_info { + my $type = shift || ''; + my @bugs; + + if ($type eq "wnpp") { + if (@wnpp_to_tag) { + @bugs = @wnpp_to_tag; + } else { + return; + } + } else { + @bugs = @to_tag; + } + + print "$progname info: "; + + if ($opt_noact) { + print "would tag"; + } else { + print "tagging"; + } + + print " these"; + print " wnpp" if $type eq "wnpp"; + print " bugs pending"; + print " and confirmed" if $opt_confirm and $type ne "wnpp"; + print ":"; + + foreach my $bug (@bugs) { + print " $bug"; + } + + print "\n"; +} + +sub help { + print <<"EOF"; +Usage: $progname [options] + +Valid options are: + --help, -h Display this message + --version Display version and copyright info + -n, --noact Only simulate what would happen during this run; + do not tag any bugs. + -s, --silent Silent mode + -v, --verbose Verbose mode: List bugs checked/tagged. + NOTE: Verbose and silent mode can't be used together. + -f, --force Do not query the BTS; (re-)tag all bug reports. + --comments Add the changelog header line and entries relating + to the bugs to be tagged to the generated mail. + (Default) + --no-comments Do not add changelog entries to the mail + -c, --confirm Tag bugs as confirmed as well as pending + -t, --to <version> Use changelog information from all versions strictly + later than <version> (mimics dpkg-parsechangelog's + -v option.) + -i, --interactive Display the message which would be sent to the BTS + and, except if --noact was used, prompt for + confirmation before sending it. + -w, --wnpp For each potentially not owned bug, check whether + it is filed against wnpp and, if so, tag it. This + allows e.g. ITA or ITPs to be tagged. + +EOF +} + +sub version { + print <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +Copyright 2008 by Adam D. Barratt <adam\@adam-barratt.org.uk>; based +on the shell script by Joshua Kwan <joshk\@triplehelix.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 (at your option) any +later version. +EOF +} + +=head1 COPYRIGHT + +This program is Copyright 2008 by Adam D. Barratt +<adam@adam-barratt.org.uk>. + +The shell script tagpending, on which this program is based, is +Copyright 2004 by Joshua Kwan <joshk@triplehelix.org> with changes +copyright 2004-7 by their respective authors. + +This program is licensed under the terms of the GPL, either version 2 of +the License, or (at your option) any later version. + +=cut diff --git a/scripts/transition-check.pl b/scripts/transition-check.pl new file mode 100755 index 0000000..25a85d3 --- /dev/null +++ b/scripts/transition-check.pl @@ -0,0 +1,241 @@ +#!/usr/bin/perl + +# transition-check: Check whether a given source package is involved +# in a current transition for which uploads have been blocked by the +# Debian release team +# +# Copyright 2008 Adam D. Barratt +# +# 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 + +transition-check - check a package list for involvement in transitions + +=head1 SYNOPSIS + +B<transition-check> B<--help>|B<--version> + +B<transition-check> [B<-f>|B<--filename=>I<FILENAME>] [I<source package list>] + +=head1 DESCRIPTION + +B<transition-check> checks whether any of the listed source packages +are involved in a transition for which uploads to unstable are currently +blocked. + +If neither a filename nor a list of packages is supplied, B<transition-check> +will use the source package name from I<debian/control>. + +=head1 OPTIONS + +=over 4 + +=item B<-f>, B<--filename=>I<filename> + +Read a source package name from I<filename>, which should be a Debian +package control file or I<.changes> file, and add that package to the list +of packages to check. + +=back + +=head1 EXIT STATUS + +The exit status indicates whether any of the packages examined were found to +be involved in a transition. + +=over 4 + +=item 0Z<> + +Either B<--help> or B<--version> was used, or none of the packages examined +was involved in a transition. + +=item 1Z<> + +At least one package examined is involved in a current transition. + +=back + +=head1 LICENSE + +This code is copyright by Adam D. Barratt <I<adam@adam-barratt.org.uk>>, +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 + +Adam D. Barratt <I<adam@adam-barratt.org.uk>> + +=cut + +use warnings; +use strict; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use File::Basename; + +my $progname = basename($0); + +my ($opt_help, $opt_version, @opt_filename); + +GetOptions( + "help|h" => \$opt_help, + "version|v" => \$opt_version, + "filename|f=s" => sub { push(@opt_filename, $_[1]); }, + ) + or die +"Usage: $progname [options] source_package_list\nRun $progname --help for more details\n"; + +if ($opt_help) { help(); exit 0; } +if ($opt_version) { version(); exit 0; } + +my ($lwp_broken, $yaml_broken); +my $ua; + +sub have_lwp() { + return ($lwp_broken ? 0 : 1) if defined $lwp_broken; + eval { + require LWP; + require LWP::UserAgent; + }; + + if ($@) { + if ($@ =~ m%^Can\'t locate LWP%) { + $lwp_broken = "the libwww-perl package is not installed"; + } else { + $lwp_broken = "couldn't load LWP::UserAgent: $@"; + } + } else { + $lwp_broken = ''; + } + return $lwp_broken ? 0 : 1; +} + +sub have_yaml() { + return ($yaml_broken ? 0 : 1) if defined $yaml_broken; + eval { require YAML::Syck; }; + + if ($@) { + if ($@ =~ m%^Can\'t locate YAML%) { + $yaml_broken = "the libyaml-syck-perl package is not installed"; + } else { + $yaml_broken = "couldn't load YAML::Syck: $@"; + } + } else { + $yaml_broken = ''; + } + return $yaml_broken ? 0 : 1; +} + +sub init_agent { + $ua = new LWP::UserAgent; # we create a global UserAgent object + $ua->agent("LWP::UserAgent/Devscripts"); + $ua->env_proxy; +} + +if (@opt_filename or !@ARGV) { + @opt_filename = ("debian/control") unless @opt_filename; + + foreach my $filename (@opt_filename) { + my $message; + + if (!@ARGV) { + $message = "No package list supplied and unable"; + } else { + $message = "Unable"; + } + + $message .= " to open $filename"; + open FILE, $filename or die "$progname: $message: $!\n"; + while (<FILE>) { + if (/^(?:Source): (.*)/) { + push(@ARGV, $1); + last; + } + } + + close FILE; + } +} + +die "$progname: Unable to retrieve transition information: $lwp_broken\n" + unless have_lwp; + +init_agent() unless $ua; +my $request = HTTP::Request->new('GET', + 'https://ftp-master.debian.org/transitions.yaml'); +my $response = $ua->request($request); +if (!$response->is_success) { + die "$progname: Failed to retrieve transitions list: $!\n"; +} + +die "$progname: Unable to parse transition information: $yaml_broken\n" + unless have_yaml(); + +my $yaml = YAML::Syck::Load($response->content); +my $packagelist = join("|", map { qq/\Q$_\E/ } @ARGV); +my $found = 0; + +foreach my $transition (keys(%{$yaml})) { + my $data = $yaml->{$transition}; + + my @affected = grep /^($packagelist)$/, @{ $data->{packages} }; + + if (@affected) { + print "\n\n" if $found; + $found = 1; + print +"The following packages are involved in the $transition transition:\n"; + print map { qq( - $_\n) } @affected; + + print "\nDetails of this transition:\n" + . " - Reason: $data->{reason}\n" + . " - Release team contact: $data->{rm}\n"; + } +} + +if (!$found) { + print "$progname: No packages examined are currently blocked\n"; +} + +exit $found; + +sub help { + print <<"EOF"; +Usage: $progname [options] source_package_list +Valid options are: + --help, -h Display this message + --version, -v Display version and copyright info + --filename, -f Read source package information from the specified + filename (which should be a Debian package control + file or changes file) +EOF +} + +sub version { + print <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +Copyright (C) 2008 by Adam D. Barratt <adam\@adam-barratt.org.uk>, + +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 +} + diff --git a/scripts/uscan.bash_completion b/scripts/uscan.bash_completion new file mode 100644 index 0000000..46aa43f --- /dev/null +++ b/scripts/uscan.bash_completion @@ -0,0 +1,59 @@ +# /usr/share/bash-completion/completions/uscan +# Bash command completion for ‘uscan(1)’. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +shopt -s progcomp + +_uscan_completion () { + COMPREPLY=() + + local cur="${COMP_WORDS[COMP_CWORD]}" + local prev="${COMP_WORDS[COMP_CWORD-1]}" + + local opts="--help --verbose -v" + opts+=" --download --safe --report --report-status" + opts+=" --signature --no-signature --skip-signature" + opts+=" --no-download --force-download -dd --overwrite-download -ddd" + opts+=" --upstream-version --download-version --download-debversion" + opts+=" --download-current-version --bare" + opts+=" --check-dirname-level --check-dirname-regex" + opts+=" --no-pasv --pasv --timeout --user-agent --useragent" + opts+=" --no-verbose --verbose --debug -vv --extra-debug -vvv" + opts+=" --no-dehs --dehs --no-conf --noconf --watchfile --destdir" + opts+=" --package --no-exclusion" + opts+=" --symlink --rename --repack --compression --copyright-file" + + case "${prev}" in + --compression) + local formats=(gzip bzip2 lzma xz) + COMPREPLY=( $(compgen -W "${formats[*]}" -- ${cur}) ) + ;; + + --check-dirname-level) + local levels=(0 1 2) + COMPREPLY=( $(compgen -W "${levels[*]}" -- ${cur}) ) + ;; + + --watchfile) + COMPREPLY=( $(compgen -A file -- ${cur}) ) + ;; + + --copyright-file) + COMPREPLY=( $(compgen -A file -- ${cur}) ) + ;; + + *) + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + compopt -o dirnames + ;; + esac +} + +complete -F _uscan_completion uscan + + +# Local variables: +# coding: utf-8 +# mode: shell-script +# End: +# vim: fileencoding=utf-8 filetype=sh : diff --git a/scripts/uscan.pl b/scripts/uscan.pl new file mode 100755 index 0000000..9a9ca5e --- /dev/null +++ b/scripts/uscan.pl @@ -0,0 +1,2220 @@ +#!/usr/bin/perl +# -*- tab-width: 8; indent-tabs-mode: t; cperl-indent-level: 4 -*- +# vim: set ai shiftwidth=4 tabstop=4 expandtab: + +# uscan: This program looks for watch files and checks upstream ftp sites +# for later versions of the software. +# +# Originally written by Christoph Lameter <clameter@debian.org> (I believe) +# Modified by Julian Gilbey <jdg@debian.org> +# HTTP support added by Piotr Roszatycki <dexter@debian.org> +# Rewritten in Perl, Copyright 2002-2006, Julian Gilbey +# Rewritten in Object Oriented Perl, copyright 2018, Xavier Guimard +# <yadd@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/>. + +####################################################################### +# {{{ code 0: POD for manpage +####################################################################### + +=pod + +=head1 NAME + +uscan - scan/watch upstream sources for new releases of software + +=head1 SYNOPSIS + +B<uscan> [I<options>] [I<path>] + +=head1 DESCRIPTION + +For basic usage, B<uscan> is executed without any arguments from the root +of the Debianized source tree where you see the F<debian/> directory, or +a directory containing multiple source trees. + +Unless --watchfile is given, B<uscan> looks recursively for valid source +trees starting from the current directory (see the below section +L<Directory name checking> for details). + +For each valid source tree found, typically the following happens: + +=over + +=item * B<uscan> reads the first entry in F<debian/changelog> to determine the +source package name I<< <spkg> >> and the last upstream version. + +=item * B<uscan> process the watch lines F<debian/watch> from the top to the +bottom in a single pass. + +=over + +=item * B<uscan> downloads a web page from the specified I<URL> in +F<debian/watch>. + +=item * B<uscan> extracts hrefs pointing to the upstream tarball(s) from the +web page using the specified I<matching-pattern> in F<debian/watch>. + +=item * B<uscan> downloads the upstream tarball with the highest version newer +than the last upstream version. + +=item * B<uscan> saves the downloaded tarball to the parent B<../> directory: +I<< ../<upkg>-<uversion>.tar.gz >> + +=item * B<uscan> invokes B<mk-origtargz> to create the source tarball: I<< +../<spkg>_<oversion>.orig.tar.gz >> + +=over + +=item * For a multiple upstream tarball (MUT) package, the secondary upstream +tarball will instead be named I<< ../<spkg>_<oversion>.orig-<component>.tar.gz >>. + +=back + +=item * Repeat until all lines in F<debian/watch> are processed. + +=back + +=item * B<uscan> invokes B<uupdate> to create the Debianized source tree: I<< +../<spkg>-<oversion>/* >> + +=back + +Please note the following. + +=over + +=item * For simplicity, the compression method used in examples is B<gzip> with +B<.gz> suffix. Other methods such as B<xz>, B<bzip2>, and B<lzma> with +corresponding B<xz>, B<bz2> and B<lzma> suffixes may also be used. + +=item * The new B<version=4> enables handling of multiple upstream tarball +(MUT) packages but this is a rare case for Debian packaging. For a single +upstream tarball package, there is only one watch line and no I<< +../<spkg>_<oversion>.orig-<component>.tar.gz >> . + +=item * B<uscan> with the B<--verbose> option produces a human readable report +of B<uscan>'s execution. + +=item * B<uscan> with the B<--debug> option produces a human readable report of +B<uscan>'s execution including internal variable states. + +=item * B<uscan> with the B<--extra-debug> option produces a human readable +report of B<uscan>'s execution including internal variable states and remote +content during "search" step. + +=item * B<uscan> with the B<--dehs> option produces an upstream package status +report in XML format for other programs such as the Debian External Health +System. + +=item * The primary objective of B<uscan> is to help identify if the latest +version upstream tarball is used or not; and to download the latest upstream +tarball. The ordering of versions is decided by B<dpkg --compare-versions>. + +=item * B<uscan> with the B<--safe> option limits the functionality of B<uscan> +to its primary objective. Both the repacking of downloaded files and +updating of the source tree are skipped to avoid running unsafe scripts. +This also changes the default to B<--no-download> and B<--skip-signature>. + +=back + +=head1 FORMAT OF THE WATCH FILE + +The current version 4 format of F<debian/watch> can be summarized as follows: + +=over + +=item * Leading spaces and tabs are dropped. + +=item * Empty lines are dropped. + +=item * A line started by B<#> (hash) is a comment line and dropped. + +=item * A single B<\> (back slash) at the end of a line is dropped and the +next line is concatenated after removing leading spaces and tabs. The +concatenated line is parsed as a single line. (The existence or non-existence +of the space before the tailing single B<\> is significant.) + +=item * The first non-comment line is: + +=over + +=item B<version=4> + +=back + +This is a required line and the recommended version number. + +If you use "B<version=3>" instead here, some features may not work as +documented here. See L<HISTORY AND UPGRADING>. + +=item * The following non-comment lines (watch lines) specify the rules for the +selection of the candidate upstream tarball URLs and are in one of the +following three formats: + +=over + +=item * B<opts="> I<...> B<"> B<http://>I<URL> I<matching-pattern> [I<version> [I<script>]] + +=item * B<http://>I<URL> I<matching-pattern> [I<version> [I<script>]] + +=item * B<opts="> I<...> B<"> + +=back + +Here, + +=over + +=item * B<opts="> I<...> B<"> specifies the behavior of B<uscan>. See L<WATCH +FILE OPTIONS>. + +=item * B<http://>I<URL> specifies the web page where upstream publishes +the link to the latest source archive. + +=over + +=item * B<https://>I<URL> may also be used, as may + +=item * B<ftp://>I<URL> + +=item * Some parts of I<URL> may be in the regex match pattern surrounded +between B<(> and B<)> such as B</foo/bar-([\.\d]+)/>. (If multiple +directories match, the highest version is picked.) Otherwise, the I<URL> +is taken as verbatim. + +=back + +=item * I<matching-pattern> specifies the full string matching pattern for +hrefs in the web page. See L<WATCH FILE EXAMPLES>. + +=over + +=item * All matching parts in B<(> and B<)> are concatenated with B<.> (period) +to form the upstream version. + +=item * If the hrefs do not contain directories, you can combine this with the +previous entry. I.e., B<http://>I<URL>B</>I<matching-pattern> . + +=back + +=item * I<version> restricts the upstream tarball which may be downloaded. +The newest available version is chosen in each case. + +=over + +=item * B<debian> I<(default)> requires the downloading upstream tarball to be +newer than the version obtained from F<debian/changelog>. + +=item * I<version-number> such as B<12.5> requires the upstream +tarball to be newer than the I<version-number>. + +=item * B<same> requires the downloaded version of the secondary tarballs to be +exactly the same as the one for the first upstream tarball downloaded. (Useful +only for MUT) + +=item * B<previous> restricts the version of the signature +file. (Used with pgpmode=previous) + +=item * B<ignore> does not restrict the version of the secondary +tarballs. (Maybe useful for MUT) + +=item * B<group> requires the downloading upstream tarball to be newer than +the version obtained from F<debian/changelog>. Package version is the +concatenation of all "group" upstream version. + +=item * B<checksum> requires the downloading upstream tarball to be newer than +the version obtained from F<debian/changelog>. Package version is the +concatenation of the version of the main tarball, followed by a checksum of all +the tarballs using the "checksum" version system. +At least the main upstream source has to be declared as "group". + +=back + +=item * I<script> is executed at the end of B<uscan> execution with appropriate +arguments provided by B<uscan> I<(default: no action)>. + +=over + +=item * The typical Debian package is a non-native package made from one +upstream tarball. Only a single line of the watch line in one of the first two +formats is usually used with its I<version> set to B<debian> and I<script> +set to B<uupdate>. + +=item * A native package should not specify I<script>. + +=item * A multiple upstream tarball (MUT) package should specify B<uupdate> +as I<script> in the last watch line and should skip specifying I<script> in the +rest of the watch lines. + +=back + +=item * The last format of the watch line is useful to set the persistent +parameters: B<user-agent>, B<compression>. If this format is used, this must +be followed by the I<URL> defining watch line(s). + +=item * [ and ] in the above format are there to mark the optional parts and +should not be typed. + +=back + +=back + +There are a few special strings which are substituted by B<uscan> to make it easy +to write the watch file. + +=over + +=item B<@PACKAGE@> + +This is substituted with the source package name found in the first line of the +F<debian/changelog> file. + +=item B<@ANY_VERSION@> + +This is substituted by the legal upstream version regex (capturing). + + [-_]?[Vv]?(\d[\-+\.:\~\da-zA-Z]*) + +=item B<@ARCHIVE_EXT@> + +This is substituted by the typical archive file extension regex (non-capturing). + + (?i)(?:\.(?:tar\.xz|tar\.bz2|tar\.gz|tar\.zstd?|zip|tgz|tbz|txz)) + +=item B<@SIGNATURE_EXT@> + +This is substituted by the typical signature file extension regex (non-capturing). + + (?i)(?:\.(?:tar\.xz|tar\.bz2|tar\.gz|tar\.zstd?|zip|tgz|tbz|txz))'(?:\.(?:asc|pgp|gpg|sig|sign))' + +=item B<@DEB_EXT@> + +This is substituted by the typical Debian extension regexp (capturing). + + [\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$ + +=back + +Some file extensions are not included in the above intentionally to avoid false +positives. You can still set such file extension patterns manually. + +=head1 WATCH FILE OPTIONS + +B<uscan> reads the watch options specified in B<opts="> I<...> B<"> to +customize its behavior. Multiple options I<option1>, I<option2>, I<option3>, +... can be set as B<opts=">I<option1>B<,> I<option2>B<,> I<option3>B<,> I< ... +>B<"> . The double quotes are necessary if options contain any spaces. + +Unless otherwise noted as persistent, most options are valid only within their +containing watch line. + +The available watch options are: + +=over + +=item B<component=>I<component> + +Set the name of the secondary source tarball as I<< +<spkg>_<oversion>.orig-<component>.tar.gz >> for a MUT package. + +=item B<ctype=>I<component-type> + +Set the type of component I<(only "nodejs" and "perl" are available for now)>. +This will help uscan to find current version if component version is ignored. + +When using B<ctype=nodejs>, uscan tries to find a version in C<package.json>, +when using B<ctype=perl>, uscan tries to find a version in C<META.json>. +If a version is found, it is used as current version for this component, +regardless version found in Debian version string. This permits a better +change detection when using I<ignore> or I<checksum> as Debian version. + +=item B<compression=>I<method> + +Set the compression I<method> when the tarball is repacked (persistent). + +Available I<method> values are what mk-origtargz supports, so B<xz>, B<gzip> +(alias B<gz>), B<bzip2> (alias B<bz2>), B<lzma>, B<default>. The default method +is currently B<xz>. +When uscan is launched in a debian source repository which format is "1.0" or +undefined, the method switches to B<gzip>. + +Please note the repacking of the upstream tarballs by B<mk-origtargz> happens +only if one of the following conditions is satisfied: + +=over + +=item * B<USCAN_REPACK> is set in the devscript configuration. See L<DEVSCRIPT +CONFIGURATION VARIABLES>. + +=item * B<--repack> is set on the commandline. See <COMMANDLINE OPTIONS>. + +=item * B<repack> is set in the watch line as B<opts="repack,>I<...>B<">. + +=item * The upstream archive is of B<zip> type including B<jar>, B<xpi>, ... + +=item * The upstream archive is of B<zstd> (Zstandard) type. + +=item * B<Files-Excluded> or B<Files-Excluded->I<component> stanzas are set in +F<debian/copyright> to make B<mk-origtargz> invoked from B<uscan> remove +files from the upstream tarball and repack it. See L<COPYRIGHT FILE +EXAMPLES> and mk-origtargz(1). + +=back + +=item B<repack> + +Force repacking of the upstream tarball using the compression I<method>. + +=item B<repacksuffix=>I<suffix> + +Add I<suffix> to the Debian package upstream version only when the +source tarball is repackaged. This rule should be used only for a single +upstream tarball package. + +=item B<mode=>I<mode> + +Set the archive download I<mode>. + +=over + +=item B<LWP> + +This mode is the default one which downloads the specified tarball from the +archive URL on the web. Automatically internal B<mode> value is updated to +either B<http> or B<ftp> by URL. + +=item B<git> + +This mode accesses the upstream git archive directly with the B<git> command +and packs the source tree with the specified tag via I<matching-pattern> into +I<spkg-version>B<.tar.xz>. + +If the upstream publishes the released tarball via its web interface, please +use it instead of using this mode. This mode is the last resort method. + +For git mode, I<matching-pattern> specifies the full string matching pattern for +tags instead of hrefs. If I<matching-pattern> is set to +B<refs/tags/>I<tag-matching-pattern>, B<uscan> downloads source from the +B<refs/tags/>I<matched-tag> of the git repository. The upstream version is +extracted from concatenating the matched parts in B<(> ... B<)> with B<.> . See +L<WATCH FILE EXAMPLES>. + +If I<matching-pattern> is set to B<HEAD>, B<uscan> downloads source from the +B<HEAD> of the git repository and the pertinent I<version> is automatically +generated with the date and hash of the B<HEAD> of the git repository. + +If I<matching-pattern> is set to B<refs/heads/>I<branch>, B<uscan> downloads source +from the named I<branch> of the git repository. + +The local repository is temporarily created as a bare git repository directory +under the destination directory where the downloaded archive is generated. This +is normally erased after the B<uscan> execution. This local repository is kept +if B<--debug> option is used. + +If the current directory is a git repository and the searched repository is +listed among the registered "remotes", then uscan will use it instead of cloning +separately. The only local change is that uscan will run a "fetch" command to +refresh the repository. + +=item B<svn> + +This mode accesses the upstream Subversion archive directly with the B<svn> +command and packs the source tree. + +For svn mode, I<matching-pattern> specifies the full string matching pattern for +directories under Subversion repository directory, specified via URL. The +upstream version is extracted from concatenating the matched parts in B<(> ... +B<)> with B<.> . + +If I<matching-pattern> is set to B<HEAD>, B<uscan> downloads the latest source +tree of the URL. The upstream version is then constructed by appending the last +revision of the URL to B<0.0~svn>. + +As commit signing is not possible with Subversion, the default B<pgpmode> is set +to B<none> when B<mode=svn>. Settings of B<pgpmode> other than B<default> and +B<none> are reported as errors. + +=back + +=item B<pretty=>I<rule> + +Set the upstream version string to an arbitrary format as an optional B<opts> +argument when the I<matching-pattern> is B<HEAD> or B<heads/>I<branch> for +B<git> mode. For the exact syntax, see the B<git-log> manpage under B<tformat>. +The default is B<pretty=0.0~git%cd.%h>. No B<uversionmangle> rules is +applicable for this case. + +When B<pretty=describe> is used, the upstream version string is the output of +the "B<git describe --tags | sed s/-/./g>" command instead. For example, if the +commit is the B<5>-th after the last tag B<v2.17.12> and its short hash is +B<ged992511>, then the string is B<v2.17.12.5.ged992511> . For this case, it is +good idea to add B<uversionmangle=s/^/0.0~/> or B<uversionmangle=s/^v//> to make +the upstream version string compatible with Debian. Please note that in order +for B<pretty=describe> to function well, upstream need to avoid tagging with +random alphabetic tags. + +The B<pretty=describe> forces to set B<gitmode=full> to make a full local clone +of the repository automatically. + +=item B<date=>I<rule> + +Set the date string used by the B<pretty> option to an arbitrary format as an +optional B<opts> argument when the I<matching-pattern> is B<HEAD> or +B<heads/>I<branch> for B<git> mode. For the exact syntax, see the +B<strftime> manpage. The default is B<date=%Y%m%d>. + +=item B<gitexport=>I<mode> + +Set the git archive export operation I<mode>. The default is +B<gitexport=default>. Set this to B<gitexport=all> to include all files in the +.orig.tar archive, ignoring any I<export-ignore> git attributes defined by the +upstream. + +This option is valid only in git mode. + +=item B<gitmode=>I<mode> + +Set the git clone operation I<mode>. The default is B<gitmode=shallow>. For +some dumb git server, you may need to manually set B<gitmode=full> to force full +clone operation. + +If the current directory is a git repository and the searched repository is +listed among the registered "remotes", then uscan will use it instead of cloning +separately. + +=item B<pgpmode=>I<mode> + +Set the PGP/GPG signature verification I<mode>. + +=over + +=item B<auto> + +B<uscan> checks possible URLs for the signature file and autogenerates a +B<pgpsigurlmangle> rule to use it. + +=item B<default> + +Use B<pgpsigurlmangle=>I<rules> to generate the candidate upstream signature +file URL string from the upstream tarball URL. (default) + +If the specified B<pgpsigurlmangle> is missing, B<uscan> checks possible URLs +for the signature file and suggests adding a B<pgpsigurlmangle> rule. + +=item B<mangle> + +Use B<pgpsigurlmangle=>I<rules> to generate the candidate upstream signature +file URL string from the upstream tarball URL. + +=item B<next> + +Verify this downloaded tarball file with the signature file specified in the +next watch line. The next watch line must be B<pgpmode=previous>. Otherwise, +no verification occurs. + +=item B<previous> + +Verify the downloaded tarball file specified in the previous watch line with +this signature file. The previous watch line must be B<pgpmode=next>. + +=item B<self> + +Verify the downloaded file I<foo.ext> with its self signature and extract its +content tarball file as I<foo>. + +=item B<gittag> + +Verify tag signature if B<mode=git>. + +=item B<none> + +No signature available. (No warning.) + +=back + +=item B<searchmode=>I<mode> + +Set the parsing search mode. + +=over + +=item B<html> I<(default)>: search pattern in "href" parameter of E<lt>aE<gt> +HTML tags + +=item B<plain>: search pattern in the full page + +This is useful if page content is not HTML but JSON. Example with +npmjs.com: + + version=4 + opts="searchmode=plain" \ + https://registry.npmjs.org/aes-js \ + https://registry.npmjs.org/aes-js/-/aes-js-(\d[\d\.]*)@ARCHIVE_EXT@ + +=back + +=item B<decompress> + +Decompress compressed archive before the pgp/gpg signature verification. + +=item B<bare> + +Disable all site specific special case code such as URL redirector uses and +page content alterations. (persistent) + +=item B<user-agent=>I<user-agent-string> + +Set the user-agent string used to contact the HTTP(S) server as +I<user-agent-string>. (persistent) + +B<user-agent> option should be specified by itself in the watch line without +I<URL>, to allow using semicolons and commas in it. + +=item B<pasv>, B<passive> + +Use PASV mode for the FTP connection. + +If PASV mode is required due to the client side network environment, set +B<uscan> to use PASV mode via L<COMMANDLINE OPTIONS> or L<DEVSCRIPT +CONFIGURATION VARIABLES> instead. + +=item B<active>, B<nopasv> + +Don't use PASV mode for the FTP connection. + +=item B<unzipopt=>I<options> + +Add the extra options to use with the B<unzip> command, such as B<-a>, B<-aa>, +and B<-b>, when executed by B<mk-origtargz>. + +=item B<dversionmangle=>I<rules> + +Normalize the last upstream version string found in F<debian/changelog> to +compare it to the available upstream tarball version. Removal of the Debian +specific suffix such as B<s/@DEB_EXT@//> is usually done here. + +You can also use B<dversionmangle=auto>, this is exactly the same than +B<dversionmangle=s/@DEB_EXT@//> + +=item B<dirversionmangle=>I<rules> + +Normalize the directory path string matching the regex in a set of parentheses +of B<http://>I<URL> as the sortable version index string. This is used as the +directory path sorting index only. + +Substitution such as B<s/PRE/~pre/; s/RC/~rc/> may help. + +=item B<pagemangle=>I<rules> + +Normalize the downloaded web page string. (Don't use this unless this is +absolutely needed. Generally, B<g> flag is required for these I<rules>.) + +This is handy if you wish to access Amazon AWS or Subversion repositories in +which <a href="..."> is not used. + +=item B<uversionmangle=>I<rules> + +Normalize the candidate upstream version strings extracted from hrefs in the +source of the web page. This is used as the version sorting index when +selecting the latest upstream version. + +Substitution such as B<s/PRE/~pre/; s/RC/~rc/> may help. + +=item B<versionmangle=>I<rules> + +Syntactic shorthand for B<uversionmangle=>I<rules>B<, dversionmangle=>I<rules> + +=item B<hrefdecode=percent-encoding> + +Convert the selected upstream tarball href string from the percent-encoded +hexadecimal string to the decoded normal URL string for obfuscated web sites. +Only B<percent-encoding> is available and it is decoded with +B<s/%([A-Fa-f\d]{2})/chr hex $1/eg>. + +=item B<downloadurlmangle=>I<rules> + +Convert the selected upstream tarball href string into the accessible URL for +obfuscated web sites. This is run after B<hrefdecode>. + +=item B<filenamemangle=>I<rules> + +Generate the upstream tarball filename from the selected href string if +I<matching-pattern> can extract the latest upstream version I<< <uversion> >> +from the selected href string. Otherwise, generate the upstream tarball +filename from its full URL string and set the missing I<< <uversion> >> from +the generated upstream tarball filename. + +Without this option, the default upstream tarball filename is generated by +taking the last component of the URL and removing everything after any '?' or +'#'. + +=item B<pgpsigurlmangle=>I<rules> + +Generate the candidate upstream signature file URL string from the upstream +tarball URL. + +=item B<oversionmangle=>I<rules> + +Generate the version string I<< <oversion> >> of the source tarball I<< +<spkg>_<oversion>.orig.tar.gz >> from I<< <uversion> >>. This should be used +to add a suffix such as B<+dfsg> to a MUT package. + +=back + +Here, the mangling rules apply the I<rules> to the pertinent string. Multiple +rules can be specified in a mangling rule string by making a concatenated +string of each mangling I<rule> separated by B<;> (semicolon). + +Each mangling I<rule> cannot contain B<;> (semicolon), B<,> (comma), or B<"> +(double quote). + +Each mangling I<rule> behaves as if a Perl command "I<$string> B<=~> I<rule>" +is executed. There are some notable details. + +=over + +=item * I<rule> may only use the B<s>, B<tr>, and B<y> operations. + +=over + +=item B<s/>I<regex>B</>I<replacement>B</>I<options> + +Regex pattern match and replace the target string. Only the B<g>, B<i> and +B<x> flags are available. Use the B<$1> syntax for back references (No +B<\1> syntax). Code execution is not allowed (i.e. no B<(?{})> or B<(??{})> +constructs). + +=item B<y/>I<source>B</>I<dest>B</> or B<tr/>I<source>B</>I<dest>B</> + +Transliterate the characters in the target string. + +=back + +=back + +=head1 EXAMPLE OF EXECUTION + +B<uscan> reads the first entry in F<debian/changelog> to determine the source +package name and the last upstream version. + +For example, if the first entry of F<debian/changelog> is: + +=over + +=item * I<< bar >> (B<3:2.03+dfsg-4>) unstable; urgency=low + +=back + +then, the source package name is I<< bar >> and the last Debian package version +is B<3:2.03+dfsg-4>. + +The last upstream version is normalized to B<2.03+dfsg> by removing the epoch +and the Debian revision. + +If the B<dversionmangle> rule exists, the last upstream version is further +normalized by applying this rule to it. For example, if the last upstream +version is B<2.03+dfsg> indicating the source tarball is repackaged, the +suffix B<+dfsg> is removed by the string substitution B<s/\+dfsg\d*$//> to +make the (dversionmangled) last upstream version B<2.03> and it is compared to +the candidate upstream tarball versions such as B<2.03>, B<2.04>, ... found in +the remote site. Thus, set this rule as: + +=over + +=item * B<opts="dversionmangle=s/\+dfsg\d*$//"> + +=back + +B<uscan> downloads a web page from B<http://>I<URL> specified in +F<debian/watch>. + +=over + +=item * If the directory name part of I<URL> has no parentheses, B<(> and B<)>, +it is taken as verbatim. + +=item * If the directory name part of I<URL> has parentheses, B<(> and B<)>, +then B<uscan> recursively searches all possible directories to find a page for +the newest version. If the B<dirversionmangle> rule exists, the generated +sorting index is used to find the newest version. If a specific version is +specified for the download, the matching version string has priority over the +newest version. + +=back + +For example, this B<http://>I<URL> may be specified as: + +=over + +=item * B<http://www.example.org/@ANY_VERSION@/> + +=back + +Please note the trailing B</> in the above to make B<@ANY_VERSION@> as the +directory. + +If the B<pagemangle> rule exists, the whole downloaded web page as a string is +normalized by applying this rule to it. This is very powerful tool and needs +to be used with caution. If other mangling rules can be used to address your +objective, do not use this rule. + +The downloaded web page is scanned for hrefs defined in the B<< <a href=" >> +I<...> B<< "> >> tag to locate the candidate upstream tarball hrefs. These +candidate upstream tarball hrefs are matched by the Perl regex pattern +I<matching-pattern> such as B<< DL-(?:[\d\.]+?)/foo-(.+)\.tar\.gz >> to narrow +down the candidates. This pattern match needs to be anchored at the beginning +and the end. For example, candidate hrefs may be: + +=over + +=item * B<< DL-2.02/foo-2.02.tar.gz >> + +=item * B<< DL-2.03/foo-2.03.tar.gz >> + +=item * B<< DL-2.04/foo-2.04.tar.gz >> + +=back + +Here the matching string of B<(.+)> in I<matching-pattern> is considered as the +candidate upstream version. If there are multiple matching strings of +capturing patterns in I<matching-pattern>, they are all concatenated with B<.> +(period) to form the candidate upstream version. Make sure to use the +non-capturing regex such as B<(?:[\d\.]+?)> instead for the variable text +matching part unrelated to the version. + +Then, the candidate upstream versions are: + +=over + +=item * B<2.02> + +=item * B<2.03> + +=item * B<2.04> + +=back + +The downloaded tarball filename is basically set to the same as the filename in +the remote URL of the selected href. + +If the B<uversionmangle> rule exists, the candidate upstream versions are +normalized by applying this rule to them. (This rule may be useful if the +upstream version scheme doesn't sort correctly to identify the newest version.) + +The upstream tarball href corresponding to the newest (uversionmangled) +candidate upstream version newer than the (dversionmangled) last upstream +version is selected. + +If multiple upstream tarball hrefs corresponding to a single version with +different extensions exist, the highest compression one is chosen. (Priority: +B<< tar.xz > tar.lzma > tar.bz2 > tar.gz >>.) + +If the selected upstream tarball href is the relative URL, it is converted to +the absolute URL using the base URL of the web page. If the B<< <base href=" +>> I< ... > B<< "> >> tag exists in the web page, the selected upstream tarball +href is converted to the absolute URL using the specified base URL in the base +tag, instead. + +If the B<downloadurlmangle> rule exists, the selected upstream tarball href is +normalized by applying this rule to it. (This is useful for some sites with the +obfuscated download URL.) + +If the B<filenamemangle> rule exists, the downloaded tarball filename is +generated by applying this rule to the selected href if I<matching-pattern> can +extract the latest upstream version I<< <uversion> >> from the selected href +string. Otherwise, generate the upstream tarball filename from its full URL +string and set the missing I<< <uversion> >> from the generated upstream +tarball filename. + +Without the B<filenamemangle> rule, the default upstream tarball filename is +generated by taking the last component of the URL and removing everything after +any '?' or '#'. + +B<uscan> downloads the selected upstream tarball to the parent B<../> +directory. For example, the downloaded file may be: + +=over + +=item * F<../foo-2.04.tar.gz> + +=back + +Let's call this downloaded version B<2.04> in the above example generically as +I<< <uversion> >> in the following. + +If the B<pgpsigurlmangle> rule exists, the upstream signature file URL is +generated by applying this rule to the (downloadurlmangled) selected upstream +tarball href and the signature file is tried to be downloaded from it. + +If the B<pgpsigurlmangle> rule doesn't exist, B<uscan> warns user if the +matching upstream signature file is available from the same URL with their +filename being suffixed by the 5 common suffix B<asc>, B<gpg>, B<pgp>, B<sig> +and B<sign>. (You can avoid this warning by setting B<pgpmode=none>.) + +If the signature file is downloaded, the downloaded upstream tarball is checked +for its authenticity against the downloaded signature file using the armored keyring +F<debian/upstream/signing-key.asc> (see L<KEYRING FILE EXAMPLES>). If its +signature is not valid, or not made by one of the listed keys, B<uscan> will +report an error. + +If the B<oversionmangle> rule exists, the source tarball version I<oversion> is +generated from the downloaded upstream version I<uversion> by applying this +rule. This rule is useful to add suffix such as B<+dfsg> to the version of all +the source packages of the MUT package for which the repacksuffix mechanism +doesn't work. + +B<uscan> invokes B<mk-origtargz> to create the source tarball properly named +for the source package with B<.orig.> (or B<< .orig-<component>. >> for the +secondary tarballs) in its filename. + +=over + +=item case A: packaging of the upstream tarball as is + +B<mk-origtargz> creates a symlink I<< ../bar_<oversion>.orig.tar.gz >> +linked to the downloaded local upstream tarball. Here, I<< bar >> is the source +package name found in F<debian/changelog>. The generated symlink may be: + +=over + +=item * F<../bar_2.04.orig.tar.gz> -> F<foo-2.04.tar.gz> (as is) + +=back + +Usually, there is no need to set up B<opts="dversionmangle=> I<...> B<"> for +this case. + +=item case B: packaging of the upstream tarball after removing non-DFSG files + +B<mk-origtargz> checks the filename glob of the B<Files-Excluded> stanza in the +first section of F<debian/copyright>, removes matching files to create a +repacked upstream tarball. Normally, the repacked upstream tarball is renamed +with I<suffix> to I<< ../bar_<oversion><suffix>.orig.tar.gz >> using +the B<repacksuffix> option for the single upstream package. Here I<< <oversion> >> +is updated to be I<< <oversion><suffix> >>. + +The removal of files is required if files are not DFSG-compliant. For such +case, B<+dfsg> is used as I<suffix>. + +So the combined options are set as +B<opts="dversionmangle=s/\+dfsg\d*$// ,repacksuffix=+dfsg">, instead. + +For example, the repacked upstream tarball may be: + +=over + +=item * F<../bar_2.04+dfsg.orig.tar.gz> (repackaged) + +=back + +=back + +B<uscan> normally invokes "B<uupdate> B<--find --upstream-version> I<oversion> +" for the version=4 watch file. + +Please note that B<--find> option is used here since B<mk-origtargz> has been +invoked to make B<*.orig.tar.gz> file already. B<uscan> picks I<< bar >> from +F<debian/changelog>. + +It creates the new upstream source tree under the I<< ../bar-<oversion> >> +directory and Debianize it leveraging the last package contents. + +=head1 WATCH FILE EXAMPLES + +When writing the watch file, you should rely on the latest upstream source +announcement web page. You should not try to second guess the upstream archive +structure if possible. Here are the typical F<debian/watch> files. + +Please note that executing B<uscan> with B<-v> or B<-vv> reveals what exactly +happens internally. + +The existence and non-existence of a space the before tailing B<\> (back slash) +are significant. + +Some undocumented shorter configuration strings are used in the below EXAMPLES +to help you with typing. These are intentional ones. B<uscan> is written to +accept such common sense abbreviations but don't push the limit. + +=head2 HTTP site (basic) + +Here is an example for the basic single upstream tarball. + + version=4 + http://example.com/~user/release/@PACKAGE@.html \ + files/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ + +Or without using the substitution strings (not recommended): + http://example.com/~user/release/foo.html \ + files/foo-([\d\.]+)\.tar\.gz + + version=4 + +For the upstream source package B<foo-2.0.tar.gz>, this watch file downloads +and creates the Debian B<orig.tar> file B<foo_2.0.orig.tar.gz>. + +=head2 HTTP site (pgpsigurlmangle) + +Here is an example for the basic single upstream tarball with the matching +signature file in the same file path. + + version=4 + opts="pgpsigurlmangle=s%$%.asc%" http://example.com/release/@PACKAGE@.html \ + files/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ + +For the upstream source package B<foo-2.0.tar.gz> and the upstream signature +file B<foo-2.0.tar.gz.asc>, this watch file downloads these files, verifies the +authenticity using the keyring F<debian/upstream/signing-key.asc> and creates the +Debian B<orig.tar> file B<foo_2.0.orig.tar.gz>. + +Here is another example for the basic single upstream tarball with the matching +signature file on decompressed tarball in the same file path. + + version=4 + opts="pgpsigurlmangle=s%@ARCHIVE_EXT@$%.asc%,decompress" \ + http://example.com/release/@PACKAGE@.html \ + files/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ + +For the upstream source package B<foo-2.0.tar.gz> and the upstream signature +file B<foo-2.0.tar.asc>, this watch file downloads these files, verifies the +authenticity using the keyring F<debian/upstream/signing-key.asc> and creates the +Debian B<orig.tar> file B<foo_2.0.orig.tar.gz>. + +=head2 HTTP site (pgpmode=next/previous) + +Here is an example for the basic single upstream tarball with the matching +signature file in the unrelated file path. + + version=4 + opts="pgpmode=next" http://example.com/release/@PACKAGE@.html \ + files/(?:\d+)/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ debian + opts="pgpmode=previous" http://example.com/release/@PACKAGE@.html \ + files/(?:\d+)/@PACKAGE@@ANY_VERSION@@SIGNATURE_EXT@ previous + +B<(?:\d+)> part can be any random value. The tarball file can have B<53>, +while the signature file can have B<33>. + +B<([\d\.]+)> part for the signature file has a strict requirement to match that +for the upstream tarball specified in the previous line by having B<previous> +as I<version> in the watch line. + +=head2 HTTP site (flexible) + +Here is an example for the maximum flexibility of upstream tarball and +signature file extensions. + + version=4 + opts="pgpmode=next" http://example.com/DL/ \ + files/(?:\d+)/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ debian + opts="pgpmode=previous" http://example.com/DL/ \ + files/(?:\d+)/@PACKAGE@@ANY_VERSION@@SIGNATURE_EXT@ \ + previous + +=head2 HTTP site (basic MUT) + +Here is an example for the basic multiple upstream tarballs. + + version=4 + opts="pgpsigurlmangle=s%$%.sig%" \ + http://example.com/release/foo.html \ + files/foo-@ANY_VERSION@@ARCHIVE_EXT@ debian + opts="pgpsigurlmangle=s%$%.sig%, component=bar" \ + http://example.com/release/foo.html \ + files/foobar-@ANY_VERSION@@ARCHIVE_EXT@ same + opts="pgpsigurlmangle=s%$%.sig%, component=baz" \ + http://example.com/release/foo.html \ + files/foobaz-@ANY_VERSION@@ARCHIVE_EXT@ same + +For the main upstream source package B<foo-2.0.tar.gz> and the secondary +upstream source packages B<foobar-2.0.tar.gz> and B<foobaz-2.0.tar.gz> which +install under F<bar/> and F<baz/>, this watch file downloads and creates the +Debian B<orig.tar> file B<foo_2.0.orig.tar.gz>, B<foo_2.0.orig-bar.tar.gz> and +B<foo_2.0.orig-baz.tar.gz>. Also, these upstream tarballs are verified by +their signature files. + +=head2 HTTP site (recursive directory scanning) + +Here is an example with the recursive directory scanning for the upstream tarball +and its signature files released in a directory named +after their version. + + version=4 + opts="pgpsigurlmangle=s%$%.sig%, dirversionmangle=s/-PRE/~pre/;s/-RC/~rc/" \ + http://tmrc.mit.edu/mirror/twisted/Twisted/@ANY_VERSION@/ \ + Twisted-@ANY_VERSION@@ARCHIVE_EXT@ + +Here, the web site should be accessible at the following URL: + + http://tmrc.mit.edu/mirror/twisted/Twisted/ + +Here, B<dirversionmangle> option is used to normalize the sorting order of the +directory names. + +=head2 HTTP site (alternative shorthand) + +For the bare HTTP site where you can directly see archive filenames, the normal +watch file: + + version=4 + opts="pgpsigurlmangle=s%$%.sig%" \ + http://www.cpan.org/modules/by-module/Text/ \ + Text-CSV_XS-@ANY_VERSION@@ARCHIVE_EXT@ + +can be rewritten in an alternative shorthand form only with a single string +covering URL and filename: + + version=4 + opts="pgpsigurlmangle=s%$%.sig%" \ + http://www.cpan.org/modules/by-module/Text/Text-CSV_XS-@ANY_VERSION@@ARCHIVE_EXT@ + +In version=4, initial white spaces are dropped. Thus, this alternative +shorthand form can also be written as: + + version=4 + opts="pgpsigurlmangle=s%$%.sig%" \ + http://www.cpan.org/modules/by-module/Text/\ + Text-CSV_XS-@ANY_VERSION@@ARCHIVE_EXT@ + +Please note the subtle difference of a space before the tailing B<\> +between the first and the last examples. + +=head2 HTTP site (funny version) + +For a site which has funny version numbers, the parenthesized groups will be +joined with B<.> (period) to make a sanitized version number. + + version=4 + http://www.site.com/pub/foobar/foobar_v(\d+)_(\d+)@ARCHIVE_EXT@ + +=head2 HTTP site (DFSG) + +The upstream part of the Debian version number can be mangled to indicate the +source package was repackaged to clean up non-DFSG files: + + version=4 + opts="dversionmangle=s/\+dfsg\d*$//,repacksuffix=+dfsg" \ + http://some.site.org/some/path/foobar-@ANY_VERSION@@ARCHIVE_EXT@ + +See L<COPYRIGHT FILE EXAMPLES>. + +=head2 HTTP site (filenamemangle) + +The upstream tarball filename is found by taking the last component of the URL +and removing everything after any '?' or '#'. + +If this does not fit to you, use B<filenamemangle>. For example, F<< <A +href="http://foo.bar.org/dl/?path=&dl=foo-0.1.1.tar.gz"> >> could be handled +as: + + version=4 + opts=filenamemangle=s/.*=(.*)/$1/ \ + http://foo.bar.org/dl/\?path=&dl=foo-@ANY_VERSION@@ARCHIVE_EXT@ + +F<< <A href="http://foo.bar.org/dl/?path=&dl_version=0.1.1"> >> +could be handled as: + + version=4 + opts=filenamemangle=s/.*=(.*)/foo-$1\.tar\.gz/ \ + http://foo.bar.org/dl/\?path=&dl_version=@ANY_VERSION@ + +If the href string has no version using <I>matching-pattern>, the version can +be obtained from the full URL using B<filenamemangle>. + + version=4 + opts=filenamemangle=s&.*/dl/(.*)/foo\.tar\.gz&foo-$1\.tar\.gz& \ + http://foo.bar.org/dl/@ANY_VERSION@/ foo.tar.gz + +=head2 HTTP site (downloadurlmangle) + +The option B<downloadurlmangle> can be used to mangle the URL of the file +to download. This can only be used with B<http://> URLs. This may be +necessary if the link given on the web page needs to be transformed in +some way into one which will work automatically, for example: + + version=4 + opts=downloadurlmangle=s/prdownload/download/ \ + http://developer.berlios.de/project/showfiles.php?group_id=2051 \ + http://prdownload.berlios.de/softdevice/vdr-softdevice-@ANY_VERSION@@ARCHIVE_EXT@ + +=head2 HTTP site (oversionmangle, MUT) + +The option B<oversionmangle> can be used to mangle the version of the source +tarball (B<.orig.tar.gz> and B<.orig-bar.tar.gz>). For example, B<+dfsg> can +be added to the upstream version as: + + version=4 + opts=oversionmangle=s/(.*)/$1+dfsg/ \ + http://example.com/~user/release/foo.html \ + files/foo-@ANY_VERSION@@ARCHIVE_EXT@ debian + opts="component=bar" \ + http://example.com/~user/release/foo.html \ + files/bar-@ANY_VERSION@@ARCHIVE_EXT@ same + +See L<COPYRIGHT FILE EXAMPLES>. + +=head2 HTTP site (pagemangle) + +The option B<pagemangle> can be used to mangle the downloaded web page before +applying other rules. The non-standard web page without proper B<< <a href=" +>> << ... >> B<< "> >> entries can be converted. For example, if F<foo.html> +uses B<< <a bogus=" >> I<< ... >> B<< "> >>, this can be converted to the +standard page format with: + + version=4 + opts=pagemangle="s/<a\s+bogus=/<a href=/g" \ + http://example.com/release/foo.html \ + files/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ + +Please note the use of B<g> here to replace all occurrences. + +If F<foo.html> uses B<< <Key> >> I<< ... >> B<< </Key> >>, this can be +converted to the standard page format with: + + version=4 + opts="pagemangle=s%<Key>([^<]*)</Key>%<Key><a href="$1">$1</a></Key>%g" \ + http://example.com/release/foo.html \ + (?:.*)/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ + +=head2 FTP site (basic): + + version=4 + ftp://ftp.tex.ac.uk/tex-archive/web/c_cpp/cweb/cweb-@ANY_VERSION@@ARCHIVE_EXT@ + +=head2 FTP site (regex special characters): + + version=4 + ftp://ftp.worldforge.org/pub/worldforge/libs/\ + Atlas-C++/transitional/Atlas-C\+\+-@ANY_VERSION@@ARCHIVE_EXT@ + +Please note that this URL is connected to be I< ... >B<libs/Atlas-C++/>I< ... > +. For B<++>, the first one in the directory path is verbatim while the one in +the filename is escaped by B<\>. + +=head2 FTP site (funny version) + +This is another way of handling site with funny version numbers, +this time using mangling. (Note that multiple groups will be +concatenated before mangling is performed, and that mangling will +only be performed on the basename version number, not any path +version numbers.) + + version=4 + opts="uversionmangle=s/^/0.0./" \ + ftp://ftp.ibiblio.org/pub/Linux/ALPHA/wine/\ + development/Wine-@ANY_VERSION@@ARCHIVE_EXT@ + +=head2 sf.net + +For SourceForge based projects, qa.debian.org runs a redirector which allows a +simpler form of URL. The format below will automatically be rewritten to use +the redirector with the watch file: + + version=4 + https://sf.net/<project>/ <tar-name>-@ANY_VERSION@@ARCHIVE_EXT@ + +For B<audacity>, set the watch file as: + + version=4 + https://sf.net/audacity/ audacity-minsrc-@ANY_VERSION@@ARCHIVE_EXT@ + +Please note, you can still use normal functionalities of B<uscan> to set up a +watch file for this site without using the redirector. + + version=4 + opts="uversionmangle=s/-pre/~pre/, \ + filenamemangle=s%(?:.*)audacity-minsrc-(.+)\.tar\.xz/download%\ + audacity-$1.tar.xz%" \ + http://sourceforge.net/projects/audacity/files/audacity/@ANY_VERSION@/ \ + (?:.*)audacity-minsrc-@ANY_VERSION@@ARCHIVE_EXT@/download + +Here, B<%> is used as the separator instead of the standard B</>. + +=head2 github.com + +For GitHub based projects, you can use the releases or tags API page. If +upstream releases properly named tarballs on their releases page, you can +search for the browser download URL (API key F<browser_download_url>): + + version=4 + opts="searchmode=plain" \ + https://api.github.com/repos/<user>/<project>/releases?per_page=100 \ + https://github.com/<user>/<project>/releases/download/[^/]+/@PACKAGE@-@ANY_VERSION@@ARCHIVE_EXT@ + +If the release page only contains the auto-generated tar.gz source code tarball, +search for the tarball URL (API key F<tarball_url>). The tarball URL uses only +the version as the filename. You can rename the downloaded upstream tarball +into the standard F<< <project>-<version>.tar.gz >> using B<filenamemangle>: + + version=4 + opts="filenamemangle=s%.*/@ANY_VERSION@%@PACKAGE@-$1.tar.gz%,searchmode=plain" \ + https://api.github.com/repos/<user>/<project>/releases?per_page=100 \ + https://api.github.com/repos/<user>/<project>/tarball/@ANY_VERSION@ + +If there are no upstream releases, you can query the equivalent tags page: + + version=4 + opts="filenamemangle=s%.*/@ANY_VERSION@%@PACKAGE@-$1.tar.gz%,searchmode=plain" \ + https://api.github.com/repos/<user>/<project>/tags?per_page=100 \ + https://api.github.com/repos/<user>/<project>/tarball/refs/tags/@ANY_VERSION@ + +If upstream releases alpha/beta tarballs, you will need to make use of the +B<uversionmangle> option: F<uversionmangle=s/(a|alpha|b|beta|c|dev|pre|rc)/~$1/> + +=head2 PyPI + +For PyPI based projects, pypi.debian.net runs a redirector which allows a +simpler form of URL. The format below will automatically be rewritten to use +the redirector with the watch file: + + version=4 + https://pypi.python.org/packages/source/<initial>/<project>/ \ + <tar-name>-@ANY_VERSION@@ARCHIVE_EXT@ + +For B<cfn-sphere>, set the watch file as: + + version=4 + https://pypi.python.org/packages/source/c/cfn-sphere/ \ + cfn-sphere-@ANY_VERSION@@ARCHIVE_EXT@ + +Please note, you can still use normal functionalities of B<uscan> to set up a +watch file for this site without using the redirector. + + version=4 + opts="pgpmode=none" \ + https://pypi.python.org/pypi/cfn-sphere/ \ + https://pypi.python.org/packages/.*/.*/.*/\ + cfn-sphere-@ANY_VERSION@@ARCHIVE_EXT@#.* + +=head2 code.google.com + +Sites which used to be hosted on the Google Code service should have migrated +to elsewhere (github?). Please look for the newer upstream site if available. + +=head2 npmjs.org (node modules) + +npmjs.org modules are published in JSON files. Here is a way to read them: + + version=4 + opts="searchmode=plain" \ + https://registry.npmjs.org/aes-js \ + https://registry.npmjs.org/aes-js/-/aes-js-@ANY_VERSION@@ARCHIVE_EXT@ + +=head2 grouped package + +Some node modules are split into multiple little upstream package. Here is +a way to group them: + + version=4 + opts="searchmode=plain,pgpmode=none" \ + https://registry.npmjs.org/mongodb \ + https://registry.npmjs.org/mongodb/-/mongodb-@ANY_VERSION@@ARCHIVE_EXT@ group + opts="searchmode=plain,pgpmode=none,component=bson" \ + https://registry.npmjs.org/bson \ + https://registry.npmjs.org/bson/-/bson-@ANY_VERSION@@ARCHIVE_EXT@ group + opts="searchmode=plain,pgpmode=none,component=mongodb-core" \ + https://registry.npmjs.org/mongodb-core \ + https://registry.npmjs.org/mongodb-core/-/mongodb-core-@ANY_VERSION@@ARCHIVE_EXT@ group + opts="searchmode=plain,pgpmode=none,component=requireoptional" \ + https://registry.npmjs.org/require_optional \ + https://registry.npmjs.org/require_optional/-/require_optional-@ANY_VERSION@@ARCHIVE_EXT@ group + +Package version is then the concatenation of upstream versions separated by +"+~". + +To avoid having a too long version, the "checksum" method can be used. +In this case, the main source has to be declared as "group": + + version=4 + opts="searchmode=plain,pgpmode=none" \ + https://registry.npmjs.org/mongodb \ + https://registry.npmjs.org/mongodb/-/mongodb-@ANY_VERSION@@ARCHIVE_EXT@ group + opts="searchmode=plain,pgpmode=none,component=bson" \ + https://registry.npmjs.org/bson \ + https://registry.npmjs.org/bson/-/bson-@ANY_VERSION@@ARCHIVE_EXT@ checksum + opts="searchmode=plain,pgpmode=none,component=mongodb-core" \ + https://registry.npmjs.org/mongodb-core \ + https://registry.npmjs.org/mongodb-core/-/mongodb-core-@ANY_VERSION@@ARCHIVE_EXT@ checksum + opts="searchmode=plain,pgpmode=none,component=requireoptional" \ + https://registry.npmjs.org/require_optional \ + https://registry.npmjs.org/require_optional/-/require_optional-@ANY_VERSION@@ARCHIVE_EXT@ checksum + +The "checksum" is made up of the separate sum of each number composing the +component versions. Following is an example with 3 components whose versions +are "1.2.4", "2.0.1" and "10.0", with the main tarball having version "2.0.6": + + Main: 2.0.6 + Comp1: 1 . 2 . 4 + Comp2: 2 . 0 . 1 + Comp3: 10 . 0 + ================================ + Result : 1+2+10 . 2+0+0 . 4+1 + Checksum: 13 . 2 . 5 + ================================ + Final Version: 2.0.6+~cs13.2.5 + +uscan will also display the original version string before being encoded into +the checksum, which can for example be used in a debian/changelog entry to +easily follow the changes: + + 2.0.6+~1.2.4+~2.0.1+~10.0 + +B<Note>: This feature currently accepts only versions composed of digits and +full stops (`.`). + +=head2 direct access to the git repository (tags) + +If the upstream only publishes its code via the git repository and its code has +no web interface to obtain the release tarball, you can use B<uscan> with the +tags of the git repository to track and package the new upstream release. + + version=4 + opts="mode=git, gitmode=full, pgpmode=none" \ + http://git.ao2.it/tweeper.git \ + refs/tags/v@ANY_VERSION@ + +Please note "B<git ls-remote>" is used to obtain references for tags. + +If a tag B<v20.5> is the newest tag, the above example downloads +I<spkg>B<-20.5.tar.xz> after making a full clone of the git repository which is +needed for dumb git server. + +If tags are signed, set B<pgpmode=gittag> to verify them. + +=head2 direct access to the git repository (HEAD) + +If the upstream only publishes its code via the git repository and its code has +no web interface nor the tags to obtain the released tarball, you can use +B<uscan> with the HEAD of the git repository to track and package the new +upstream release with an automatically generated version string. + + version=4 + opts="mode=git, pgpmode=none" \ + https://github.com/Debian/dh-make-golang \ + HEAD + +Please note that a local shallow copy of the git repository is made with "B<git +clone --bare --depth=1> ..." normally in the target directory. B<uscan> +generates the new upstream version with "B<git log --date=format:%Y%m%d +--pretty=0.0~git%cd.%h>" on this local copy of repository as its default +behavior. + +The generation of the upstream version string may the adjusted to your taste by +adding B<pretty> and B<date> options to the B<opts> arguments. + +=head2 direct access to the Subversion repository (tags) + +If the upstream only publishes its code via the Subversion repository and its +code has no web interface to obtain the release tarball, you can use B<uscan> +with the tags of the Subversion repository to track and package the new upstream +release. + + version=4 + opts="mode=svn, pgpmode=none" \ + svn://svn.code.sf.net/p/jmol/code/tags/ \ + @ANY_VERSION@\/ + +=head2 direct access to the Subversion repository (HEAD) + +If the upstream only publishes its code via the Subversion repository and its +code has no web interface to obtain the release tarball, you can use B<uscan> +to get the most recent source of a subtree in the repository with an +automatically generated version string. + + version=4 + opts="mode=svn, pgpmode=none" \ + svn://svn.code.sf.net/p/jmol/code/trunk/ \ + HEAD + +By default, B<uscan> generates the new upstream version by appending the +revision number to "0.0~svn". This can later be changed using B<uversionmangle>. + +=head1 COPYRIGHT FILE EXAMPLES + +Here is an example for the F<debian/copyright> file which initiates automatic +repackaging of the upstream tarball into I<< <spkg>_<oversion>.orig.tar.gz >> +(In F<debian/copyright>, the B<Files-Excluded> and +B<Files-Excluded->I<component> stanzas are a part of the first paragraph and +there is a blank line before the following paragraphs which contain B<Files> +and other stanzas.): + + Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + Files-Excluded: exclude-this + exclude-dir + */exclude-dir + .* + */js/jquery.js + + Files: * + Copyright: ... + ... + +Here is another example for the F<debian/copyright> file which initiates +automatic repackaging of the multiple upstream tarballs into +I<< <spkg>_<oversion>.orig.tar.gz >> and +I<< <spkg>_<oversion>.orig-bar.tar.gz >>: + + Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + Files-Excluded: exclude-this + exclude-dir + */exclude-dir + .* + */js/jquery.js + Files-Excluded-bar: exclude-this + exclude-dir + */exclude-dir + .* + */js/jquery.js + + Files: * + Copyright: ... + ... + +See mk-origtargz(1). + +=head1 KEYRING FILE EXAMPLES + +Let's assume that the upstream "B<< uscan test key (no secret) +<none@debian.org> >>" signs its package with a secret OpenPGP key and publishes +the corresponding public OpenPGP key. This public OpenPGP key can be +identified in 3 ways using the hexadecimal form. + +=over + +=item * The fingerprint as the 20 byte data calculated from the public OpenPGP +key. E. g., 'B<CF21 8F0E 7EAB F584 B7E2 0402 C77E 2D68 7254 3FAF>' + +=item * The long keyid as the last 8 byte data of the fingerprint. E. g., +'B<C77E2D6872543FAF>' + +=item * The short keyid is the last 4 byte data of the fingerprint. E. g., +'B<72543FAF>' + +=back + +Considering the existence of the collision attack on the short keyid, the use +of the long keyid is recommended for receiving keys from the public key +servers. You must verify the downloaded OpenPGP key using its full fingerprint +value which you know is the trusted one. + +The armored keyring file F<debian/upstream/signing-key.asc> can be created by +using the B<gpg> (or B<gpg2>) command as follows. + + $ gpg --recv-keys "C77E2D6872543FAF" + ... + $ gpg --finger "C77E2D6872543FAF" + pub 4096R/72543FAF 2015-09-02 + Key fingerprint = CF21 8F0E 7EAB F584 B7E2 0402 C77E 2D68 7254 3FAF + uid uscan test key (no secret) <none@debian.org> + sub 4096R/52C6ED39 2015-09-02 + $ cd path/to/<upkg>-<uversion> + $ mkdir -p debian/upstream + $ gpg --export --export-options export-minimal --armor \ + 'CF21 8F0E 7EAB F584 B7E2 0402 C77E 2D68 7254 3FAF' \ + >debian/upstream/signing-key.asc + +The binary keyring files, F<debian/upstream/signing-key.pgp> and +F<debian/upstream-signing-key.pgp>, are still supported but deprecated. + +If a group of developers sign the package, you need to list fingerprints of all +of them in the argument for B<gpg --export ...> to make the keyring to contain +all OpenPGP keys of them. + +Sometimes you may wonder who made a signature file. You can get the public +keyid used to create the detached signature file F<foo-2.0.tar.gz.asc> by +running B<gpg> as: + + $ gpg -vv foo-2.0.tar.gz.asc + gpg: armor: BEGIN PGP SIGNATURE + gpg: armor header: Version: GnuPG v1 + :signature packet: algo 1, keyid C77E2D6872543FAF + version 4, created 1445177469, md5len 0, sigclass 0x00 + digest algo 2, begin of digest 7a c7 + hashed subpkt 2 len 4 (sig created 2015-10-18) + subpkt 16 len 8 (issuer key ID C77E2D6872543FAF) + data: [4091 bits] + gpg: assuming signed data in `foo-2.0.tar.gz' + gpg: Signature made Sun 18 Oct 2015 11:11:09 PM JST using RSA key ID 72543FAF + ... + +=head1 COMMANDLINE OPTIONS + +For the basic usage, B<uscan> does not require to set these options. + +=over + +=item B<--conffile>, B<--conf-file> + +Add or replace default configuration files (C</etc/devscripts.conf> and +C<~/.devscripts>). This can only be used as the first option given on the +command-line. + +=over + +=item replace: + + uscan --conf-file test.conf --verbose + +=item add: + + uscan --conf-file +test.conf --verbose + +If one B<--conf-file> has no C<+>, default configuration files are ignored. + +=back + +=item B<--no-conf>, B<--noconf> + +Don't read any configuration files. This can only be used as the first option +given on the command-line. + +=item B<--no-verbose> + +Don't report verbose information. (default) + +=item B<--verbose>, B<-v> + +Report verbose information. + +=item B<--debug>, B<-vv> + +Report verbose information and some internal state values. + +=item B<--extra-debug>, B<-vvv> + +Report verbose information including the downloaded +web pages as processed to STDERR for debugging. + +=item B<--dehs> + +Send DEHS style output (XML-type) to STDOUT, while +send all other uscan output to STDERR. + +=item B<--no-dehs> + +Use only traditional uscan output format. (default) + +=item B<--download>, B<-d> + +Download the new upstream release. (default) + +=item B<--force-download>, B<-dd> + +Download the new upstream release even if up-to-date. (may not overwrite the local file) + +=item B<--overwrite-download>, B<-ddd> + +Download the new upstream release even if up-to-date. (may overwrite the local file) + +=item B<--no-download>, B<--nodownload> + +Don't download and report information. + +Previously downloaded tarballs may be used. + +Change default to B<--skip-signature>. + +=item B<--signature> + +Download signature. (default) + +=item B<--no-signature> + +Don't download signature but verify if already downloaded. + +=item B<--skip-signature> + +Don't bother download signature nor verifying signature. + +=item B<--safe>, B<--report> + +Avoid running unsafe scripts by skipping both the repacking of the downloaded +package and the updating of the new source tree. + +Change default to B<--no-download> and B<--skip-signature>. + +When the objective of running B<uscan> is to gather the upstream package status +under the security conscious environment, please make sure to use this option. + +=item B<--report-status> + +This is equivalent of setting "B<--verbose --safe>". + +=item B<--download-version> I<version> + +Specify the I<version> which the upstream release must match in order to be +considered, rather than using the release with the highest version. +(a best effort feature) + +=item B<--download-debversion> I<version> + +Specify the Debian package version to download the corresponding upstream +release version. The B<dversionmangle> and B<uversionmangle> rules are considered. +(a best effort feature) + +=item B<--download-current-version> + +Download the currently packaged version. +(a best effort feature) + +=item B<--check-dirname-level> I<N> + +See the below section L<Directory name checking> for an explanation of this option. + +=item B<--check-dirname-regex> I<regex> + +See the below section L<Directory name checking> for an explanation of this option. + +=item B<--destdir> I<path> +Normally, B<uscan> changes its internal current directory to the package's +source directory where the F<debian/> is located. Then the destination +directory for the downloaded tarball and other files is set to the parent +directory F<../> from this internal current directory. + +This default destination directory can be overridden by setting B<--destdir> +option to a particular I<path>. If this I<path> is a relative path, the +destination directory is determined in relative to the internal current +directory of B<uscan> execution. If this I<path> is a absolute path, the +destination directory is set to I<path> irrespective of the internal current +directory of B<uscan> execution. + +The above is true not only for the simple B<uscan> run in the single source tree +but also for the advanced scanning B<uscan> run with subdirectories holding +multiple source trees. + +One exception is when B<--watchfile> and B<--package> are used together. For +this case, the internal current directory of B<uscan> execution and the default +destination directory are set to the current directory F<.> where B<uscan> is +started. The default destination directory can be overridden by setting +B<--destdir> option as well. + +=item B<--package> I<package> + +Specify the name of the package to check for rather than examining +F<debian/changelog>; this requires the B<--upstream-version> (unless a version +is specified in the F<watch> file) and B<--watchfile> options as well. +Furthermore, no directory scanning will be done and nothing will be downloaded. +This option automatically sets B<--no-download> and B<--skip-signature>; and +probably most useful in conjunction with the DEHS system (and B<--dehs>). + +=item B<--upstream-version> I<upstream-version> + +Specify the current upstream version rather than examine F<debian/watch> or +F<debian/changelog> to determine it. This is ignored if a directory scan is being +performed and more than one F<debian/watch> file is found. + +=item B<--watchfile> I<watchfile> + +Specify the I<watchfile> rather than perform a directory scan to determine it. +If this option is used without B<--package>, then B<uscan> must be called from +within the Debian package source tree (so that F<debian/changelog> can be found +simply by stepping up through the tree). + +One exception is when B<--watchfile> and B<--package> are used together. +B<uscan> can be called from anywhare and the internal current directory of +B<uscan> execution and the default destination directory are set to the current +directory F<.> where B<uscan> is started. + +See more in the B<--destdir> explanation. + +=item B<--bare> + +Disable all site specific special case codes to perform URL redirections and +page content alterations. + +=item B<--http-header> + +Add specified header in HTTP requests for matching url. This option can be used +more than one time, values must be in the form "baseUrl@Name=value. Example: + + uscan --http-header https://example.org@My-Token=qwertyuiop + +Security: + +=over + +=item The given I<baseUrl> must exactly match the base url before '/'. +Examples: + + | --http-header value | Good for | Never used | + +------------------------------------+-----------------------------+------------+ + | https://example.org.com@Hdr=Value | https://example.org.com/... | | + | https://example.org.com/@Hdr=Value | | X | + | https://e.com:1879@Hdr=Value | https://e.com:1879/... | | + | https://e.com:1879/dir@Hdr=Value | https://e.com:1879/dir/... | | + | https://e.com:1879/dir/@Hdr=Value | | X | + +=item It is strongly recommended to not use this feature to pass a secret +token over unciphered connection I<(http://)> + +=item You can use C<USCAN_HTTP_HEADER> variable (in C<~/.devscripts>) to hide +secret token from scripts + +=back + +=item B<--no-exclusion> + +Don't automatically exclude files mentioned in F<debian/copyright> field B<Files-Excluded>. + +=item B<--pasv> + +Force PASV mode for FTP connections. + +=item B<--no-pasv> + +Don't use PASV mode for FTP connections. + +=item B<--no-symlink> + +Don't rename nor repack upstream tarball. + +=item B<--timeout> I<N> + +Set timeout to I<N> seconds (default 20 seconds). + +=item B<--user-agent>, B<--useragent> + +Override the default user agent header. + +=item B<--help> + +Give brief usage information. + +=item B<--version> + +Display version information. + +=back + +B<uscan> also accepts following options and passes them to B<mk-origtargz>: + +=over + +=item B<--symlink> + +Make B<orig.tar.gz> (with the appropriate extension) symlink to the downloaded +files. (This is the default behavior.) + +=item B<--copy> + +Instead of symlinking as described above, copy the downloaded files. + +=item B<--rename> + +Instead of symlinking as described above, rename the downloaded files. + +=item B<--repack> + +After having downloaded an lzma tar, xz tar, bzip tar, gz tar, zip, jar, xpi, +zstd archive, repack it to the specified compression (see B<--compression>). + +The unzip package must be installed in order to repack zip, jar, and xpi +archives, the xz-utils package must be installed to repack lzma or xz tar +archives, and zstd must be installed to repack zstd archives. + +=item B<--compression> [ B<gzip> | B<bzip2> | B<lzma> | B<xz> ] + +In the case where the upstream sources are repacked (either because B<--repack> +option is given or F<debian/copyright> contains the field B<Files-Excluded>), +it is possible to control the compression method via the parameter. The +default is B<gzip> for normal tarballs, and B<xz> for tarballs generated +directly from the git repository. + +=item B<--copyright-file> I<copyright-file> + +Exclude files mentioned in B<Files-Excluded> in the given I<copyright-file>. +This is useful when running B<uscan> not within a source package directory. + +=back + +=head1 DEVSCRIPT CONFIGURATION VARIABLES + +For the basic usage, B<uscan> does not require to set these 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. These +may be overridden by command line options. Environment variable settings are +ignored for this purpose. If the first command line option given is +B<--noconf>, then these files will not be read. The currently recognized +variables are: + +=over + +=item B<USCAN_DOWNLOAD> + +Download or report only: + +=over + +=item B<no>: equivalent to B<--no-download>, newer upstream files will +not be downloaded. + +=item B<yes>: equivalent to B<--download>, newer upstream files will +be downloaded. This is the default behavior. + +See also B<--force-download> and B<--overwrite-download>. + +=back + +=item B<USCAN_SAFE> + +If this is set to B<yes>, then B<uscan> avoids running unsafe scripts by +skipping both the repacking of the downloaded package and the updating of the +new source tree; this is equivalent to the B<--safe> options; this also sets +the default to B<--no-download> and B<--skip-signature>. + +=item B<USCAN_PASV> + +If this is set to yes or no, this will force FTP connections to use PASV mode +or not to, respectively. If this is set to default, then B<Net::FTP(3)> makes +the choice (primarily based on the B<FTP_PASSIVE> environment variable). + +=item B<USCAN_TIMEOUT> + +If set to a number I<N>, then set the timeout to I<N> seconds. This is +equivalent to the B<--timeout> option. + +=item B<USCAN_SYMLINK> + +If this is set to no, then a I<pkg>_I<version>B<.orig.tar.{gz|bz2|lzma|xz}> +symlink will not be made (equivalent to the B<--no-symlink> option). If it is +set to B<yes> or B<symlink>, then the symlinks will be made. If it is set to +B<rename>, then the files are renamed (equivalent to the B<--rename> option). + +=item B<USCAN_DEHS_OUTPUT> + +If this is set to B<yes>, then DEHS-style output will be used. This is +equivalent to the B<--dehs> option. + +=item B<USCAN_VERBOSE> + +If this is set to B<yes>, then verbose output will be given. This is +equivalent to the B<--verbose> option. + +=item B<USCAN_USER_AGENT> + +If set, the specified user agent string will be used in place of the default. +This is equivalent to the B<--user-agent> option. + +=item B<USCAN_DESTDIR> + +If set, the downloaded files will be placed in this directory. This is +equivalent to the B<--destdir> option. + +=item B<USCAN_REPACK> + +If this is set to yes, then after having downloaded a bzip tar, lzma tar, xz +tar, zip or zstd archive, uscan will repack it to the specified compression +(see B<--compression>). This is equivalent to the B<--repack> option. + +=item B<USCAN_EXCLUSION> + +If this is set to no, files mentioned in the field B<Files-Excluded> of +F<debian/copyright> will be ignored and no exclusion of files will be tried. +This is equivalent to the B<--no-exclusion> option. + +=item B<USCAN_HTTP_HEADER> + +If set, the specified http header will be used if URL match. This is equivalent +to B<--http-header> option. + +=back + +=head1 EXIT STATUS + +The exit status gives some indication of whether a newer version was found or +not; one is advised to read the output to determine exactly what happened and +whether there were any warnings to be noted. + +=over + +=item B<0> + +Either B<--help> or B<--version> was used, or for some F<watch> file which was +examined, a newer upstream version was located. + +=item B<1> + +No newer upstream versions were located for any of the F<watch> files examined. + +=back + +=head1 ADVANCED FEATURES + +B<uscan> has many other enhanced features which are skipped in the above +section for the simplicity. Let's check their highlights. + +B<uscan> can be executed with I<path> as its argument to change the starting +directory of search from the current directory to I<path> . + +If you are not sure what exactly is happening behind the scene, please enable +the B<--verbose> option. If this is not enough, enable the B<--debug> option +too see all the internal activities. + +See L<COMMANDLINE OPTIONS> and L<DEVSCRIPT CONFIGURATION VARIABLES> for other +variations. + +=head2 Custom script + +The optional I<script> parameter in F<debian/watch> means to execute I<script> +with options after processing this line if specified. + +See L<HISTORY AND UPGRADING> for how B<uscan> invokes the custom I<script>. + +For compatibility with other tools such as B<git-buildpackage>, it may not be +wise to create custom scripts with random behavior. In general, B<uupdate> is +the best choice for the non-native package and custom scripts, if created, +should behave as if B<uupdate>. For possible use case, see +L<http://bugs.debian.org/748474> as an example. + +=head2 URL diversion + +Some popular web sites changed their web page structure causing maintenance +problems to the watch file. There are some redirection services created to +ease maintenance of the watch file. Currently, B<uscan> makes automatic +diversion of URL requests to the following URLs to cope with this situation. + +=over + +=item * L<http://sf.net> + +=item * L<http://pypi.python.org> + +=back + +=head2 Directory name checking + +Similarly to several other scripts in the B<devscripts> package, B<uscan> +explores the requested directory trees looking for F<debian/changelog> and +F<debian/watch> files. As a safeguard against stray files causing potential +problems, and in order to promote efficiency, it will examine the name of the +parent directory once it finds the F<debian/changelog> file, and check that the +directory name corresponds to the package name. It will only attempt to +download newer versions of the package and then perform any requested action if +the directory name matches the package name. Precisely how it does this is +controlled by two configuration file variables +B<DEVSCRIPTS_CHECK_DIRNAME_LEVEL> and B<DEVSCRIPTS_CHECK_DIRNAME_REGEX>, and +their corresponding command-line options B<--check-dirname-level> and +B<--check-dirname-regex>. + +B<DEVSCRIPTS_CHECK_DIRNAME_LEVEL> can take the following values: + +=over + +=item B<0> + +Never check the directory name. + +=item B<1> + +Only check the directory name if we have had to change directory in +our search for F<debian/changelog>, that is, the directory containing +F<debian/changelog> is not the directory from which B<uscan> was invoked. +This is the default behavior. + +=item B<2> + +Always check the directory name. + +=back + +The directory name is checked by testing whether the current directory name (as +determined by pwd(1)) matches the regex given by the configuration file +option B<DEVSCRIPTS_CHECK_DIRNAME_REGEX> or by the command line option +B<--check-dirname-regex> I<regex>. Here regex is a Perl regex (see +perlre(3perl)), which will be anchored at the beginning and the end. If regex +contains a B</>, then it must match the full directory path. If not, then +it must match the full directory name. If regex contains the string I<package>, +this will be replaced by the source package name, as determined from the +F<debian/changelog>. The default value for the regex is: I<package>B<(-.+)?>, thus matching +directory names such as I<package> and I<package>-I<version>. + +=head1 HISTORY AND UPGRADING + +This section briefly describes the backwards-incompatible F<watch> file features +which have been added in each F<watch> file version, and the first version of the +B<devscripts> package which understood them. + +=over + +=item Pre-version 2 + +The F<watch> file syntax was significantly different in those days. Don't use it. +If you are upgrading from a pre-version 2 F<watch> file, you are advised to read +this manpage and to start from scratch. + +=item Version 2 + +B<devscripts> version 2.6.90: The first incarnation of the current style of +F<watch> files. This version is also deprecated and will be rejected after +the Debian 11 release. + +=item Version 3 + +B<devscripts> version 2.8.12: Introduced the following: correct handling of +regex special characters in the path part, directory/path pattern matching, +version number in several parts, version number mangling. Later versions +have also introduced URL mangling. + +If you are upgrading from version 2, the key incompatibility is if you have +multiple groups in the pattern part; whereas only the first one would be used +in version 2, they will all be used in version 3. To avoid this behavior, +change the non-version-number groups to be B<(?:> I< ...> B<)> instead of a +plain B<(> I< ... > B<)> group. + +=over + +=item * B<uscan> invokes the custom I<script> as "I<script> B<--upstream-version> +I<version> B<../>I<spkg>B<_>I<version>B<.orig.tar.gz>". + +=item * B<uscan> invokes the standard B<uupdate> as "B<uupdate> B<--no-symlink +--upstream-version> I<version> B<../>I<spkg>B<_>I<version>B<.orig.tar.gz>". + +=back + +=item Version 4 + +B<devscripts> version 2.15.10: The first incarnation of F<watch> files +supporting multiple upstream tarballs. + +The syntax of the watch file is relaxed to allow more spaces for readability. + +If you have a custom script in place of B<uupdate>, you may also encounter +problems updating from Version 3. + +=over + +=item * B<uscan> invokes the custom I<script> as "I<script> B<--upstream-version> +I<version>". + +=item * B<uscan> invokes the standard B<uupdate> as "B<uupdate> B<--find> +B<--upstream-version> I<version>". + +=back + +Restriction for B<--dehs> is lifted by redirecting other output to STDERR when +it is activated. + +=back + +=head1 SEE ALSO + +dpkg(1), mk-origtargz(1), perlre(1), uupdate(1), devscripts.conf(5) + +=head1 AUTHOR + +The original version of uscan was written by Christoph Lameter +<clameter@debian.org>. Significant improvements, changes and bugfixes were +made by Julian Gilbey <jdg@debian.org>. HTTP support was added by Piotr +Roszatycki <dexter@debian.org>. The program was rewritten in Perl by Julian +Gilbey. Xavier Guimard converted it in object-oriented Perl using L<Moo>. + +=cut + +####################################################################### +# }}} code 0: POD for manpage +####################################################################### +####################################################################### +# {{{ code 1: initializer, command parser, and loop over watchfiles +####################################################################### + +# This code block is the start up of uscan. +# Actual processing is performed by process_watchfile in the next block +# +# This has 3 different modes to process watchfiles +# +# * If $opt_watchfile and $opt_package are defined, test specified watchfile +# without changelog (sanity check for $opt_uversion may be good idea) +# * If $opt_watchfile is defined but $opt_package isn't defined, test specified +# watchfile assuming you are in source tree and debian/changelogis used to +# set variables +# * If $opt_watchfile isn't defined, scan subdirectories of directories +# specified as ARGS (if none specified, "." is scanned). +# * Normal packaging has no ARGS and uses "." +# * Archive status scanning tool uses many ARGS pointing to the expanded +# source tree to be checked. +# Comments below focus on Normal packaging case and sometimes ignores first 2 +# watch file testing setup. + +use 5.010; # defined-or (//) +use strict; +use warnings; +use Cwd qw/cwd/; +use Devscripts::Uscan::Config; +use Devscripts::Uscan::FindFiles; +use Devscripts::Uscan::Output; +use Devscripts::Uscan::WatchFile; + +our $uscan_version = "###VERSION###"; + +BEGIN { + pop @INC if $INC[-1] eq '.'; +} + +my $config = Devscripts::Uscan::Config->new->parse; + +uscan_verbose "$progname (version $uscan_version) See $progname(1) for help"; +if ($dehs) { + uscan_verbose "The --dehs option enabled.\n" + . " STDOUT = XML output for use by other programs\n" + . " STDERR = plain text output for human\n" + . " Use the redirection of STDOUT to a file to get the clean XML data"; +} + +# Did we find any new upstream versions on our wanderings? +my $res = 0; + +my @wf = find_watch_files($config); +foreach (@wf) { + my $tmp = process_watchfile(@$_); + $res ||= $tmp; + + # Are there any warnings to give if we're using dehs? + dehs_output if ($dehs); +} + +uscan_verbose "Scan finished"; + +# Are there any warnings to give if we're using dehs? +$dehs_end_output = 1; +dehs_output if ($dehs); + +exit($res ? $res : $found ? 0 : 1); + +####################################################################### +# {{{ code 2: process watchfile by looping over watchline +####################################################################### + +sub process_watchfile { + my ($pkg_dir, $package, $version, $watchfile) = @_; + my $opwd = cwd(); + chdir $pkg_dir; + + my $wf = Devscripts::Uscan::WatchFile->new({ + config => $config, + package => $package, + pkg_dir => $pkg_dir, + pkg_version => $version, + watchfile => $watchfile, + }); + return $wf->status if ($wf->status); + + my $res = $wf->process_lines; + chdir $opwd; + return $res; +} +####################################################################### +# }}} code 2: process watchfile by looping over watchline +####################################################################### diff --git a/scripts/uupdate.1 b/scripts/uupdate.1 new file mode 100644 index 0000000..cf63a0d --- /dev/null +++ b/scripts/uupdate.1 @@ -0,0 +1,199 @@ +.TH UUPDATE 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +uupdate \- upgrade a source code package from an upstream revision +.SH SYNOPSIS +\fBuupdate\fR [\fIoptions\fR] \fInew_upstream_archive\fR [\fIversion\fR] +.br +\fBuupdate\fR [\fIoptions\fR] \fB\-\-find\fR|\fB\-f\fR +.br +\fBuupdate\fR [\fIoptions\fR] \fB\-\-patch\fR|\fB\-p\fR \fIpatch_file\fR +.SH DESCRIPTION +\fBuupdate\fR modifies an existing Debian source code archive to +reflect an upstream update supplied as a patch or from a wholly new +source code archive. The utility needs to be invoked from the top +directory of the old source code directory, and if a relative name is +given for the new archive or patch file, it will be looked for first +relative to the execution directory and then relative to the parent of +the source tree. (For example, if the changelog file is +\fI/usr/local/src/foo/foo-1.1/debian/changelog\fR, then the archive or +patch file will be looked for relative to \fI/usr/local/src/foo\fR.) +Note that the patch file or archive cannot be within the source tree +itself. The full details of what the code does are given below. +.PP +Currently supported source code file types are \fI.tar.gz\fR, +\fI.tar.bz2\fR, \fI.tar.Z\fR, \fI.tgz\fR, \fI.tar\fR, \fI.tar.lzma\fR, +\fI.tar.xz\fR, \fI.7z\fR and \fI.zip\fR +archives. Also supported are already unpacked source code archives; +simply give the path of the source code directory. Supported patch +file types are \fBgzip\fR-compressed, \fBbzip2\fR-compressed, +\fBlzma\fR-compressed, \fBxz\fR-compressed and +uncompressed patch files. The file types are identified by the file +names, so they must use the standard suffixes. +.PP +Usually \fBuupdate\fR will be able to deduce the version number from +the source archive name (as long as it only contains digits and +periods). If that fails, you need to specify the version number +explicitly (without the Debian release number which will always be +initially \*(lq1\*(rq, or \*(lq0ubuntu1\*(rq on Ubuntu-detected systems). This can be +done with an initial \fB\-\-upstream-version\fR or \fB\-v\fR option, or +in the case of an archive, with a version number after the filename. +(The reason for the latter is so that \fBuupdate\fR can be called +directly from \fBuscan\fR.) +.PP +Since \fBuupdate\fR uses \fBdebuild\fR to clean the current archive +before trying to apply a patch file, it accepts a \fB\-\-rootcmd\fR or +\fB\-r\fR option allowing the user to specify a gain-root command to be +used. The default is to use \fBfakeroot\fR. +.PP +If an archive is being built, the pristine upstream source should be +used to create the \fI.orig.tar.gz\fR file wherever possible. This +means that MD5 sums or other similar methods can be used to easily +compare the upstream source to Debian's copy of the upstream version. +This is the default behaviour, and can be switched off using the +\fB\-\-no\-pristine\fR option below. +.SH OPTIONS +This is a summary of what was explained above. +.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\-\-upstream\-version \fIversion\fR, \fB\-v \fIversion\fR +Specify the version number of the upstream package explicitly. +.TP +\fB\-\-force\-bad\-version, \fB\-b +Force a version number to be less than the current one (e.g., when backporting). +.TP +\fB\-\-rootcmd \fIgain-root-command\fR, \fB\-r \fIgain-root-command\fR +Specify the command to be used to become root to build the package and +is passed onto \fBdebuild\fR(1) if it is specified. +.TP +\fB\-\-pristine\fR, \fB\-u\fR +Treat the source as pristine upstream source and symlink to it from +\fI<package>_<version>.orig.tar.gz\fR whenever possible. This option +has no meaning for patches. This is the default behaviour. +.TP +\fB\-\-no\-pristine\fR +Do not attempt to make a \fI<package>_<version>.orig.tar.gz\fR symlink. +.TP +\fB\-\-symlink\fR, \fB\-s\fR +Simply create a symlink when moving a new upstream \fI.tar.gz\fR +archive to the new \fI<package>_<version>.orig.tar.gz\fR location. +This is the default behaviour. +.TP +\fB\-\-no\-symlink\fR +Copy the upstream \fI.tar.gz\fR to the new location instead of making +a symlink, if \fI<package>_<version>.orig.tar.gz\fR is missing. +Otherwise, do nothing. +.TP +.B \-\-find, \fB\-f\fR +Find all upstream tarballs in \fI../\fR which match +\fI<pkg>_<version>.orig.tar.{gz|bz2|lzma|xz}\fR or +\fI<pkg>_<version>.orig\-<component>.tar.{gz|bz2|lzma|xz}\fR ; +\fB\-\-upstream\-version\fR required; pristine source required; +not valid for \fB\-\-patch\fR; +This option uses \fBdpkg\-source\fR as the backend to enable support for the +multiple upstream tarballs and to resolve minor bugs reported previously. The +use of this option is highly recommended. +.TP +.B \-\-verbose +Give verbose output. +.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 UUPDATE_PRISTINE +If this is set to \fIno\fR, then it is the same as the +\fB\-\-no\-pristine\fR command line parameter being used. +.TP +.B UUPDATE_SYMLINK_ORIG +If this is set to \fIno\fR, then it is the same as the +\fB\-\-no\-symlink\fR command line parameter being used. +.TP +.B UUPDATE_ROOTCMD +This is equivalent to the \fB\-\-rootcmd\fR option. +.SH "ACTIONS TAKEN ON AN ARCHIVE" +.TP +.B Figure out new version number +Unless an explicit version number is provided, the archive name is +analyzed for a sequence of digits separated by dots. If something +like that is found, it is taken to be the new upstream version +number. If not, processing is aborted. +.TP +.B Create the .orig.tar.gz archive +If the \fB\-\-pristine\fR or \fB\-u\fR option is specified and the +upstream archive is a \fI.tar.gz\fR or \fI.tgz\fR archive, then this +will be copied directly to \fI<package>_<version>.orig.tar.gz\fR. +.TP +.B Unpacking +The archive is unpacked and placed in a directory with the correct +name according to Debian policy: package-upstream_version.orig. +Processing is aborted if this directory already exists. +.TP +.B Patching +The \fI.diffs.gz\fR from the current version are applied to the +unpackaged archive. A non-zero exit status and warning message will +occur if the patches did not apply cleanly or if no patch file was +found. Also, the list of rejected patches will be shown. The +file \fIdebian/rules\fR is made executable and all of the \fI.orig\fR +files created by \fBpatch\fR are deleted. +.TP +.B Changelog update +A changelog entry with the new version number is generated with the +text \*(lqNew upstream release.\*(rq. + +When used on Ubuntu systems, \fBdpkg-vendor\fR detection is used to set +the Debian revision to \*(lq0ubuntu1\*(rq. You may change +\fIdebian/changelog\fR manually afterwards. +.SH "ACTIONS TAKEN ON A PATCH FILE" +.TP +.B Figure out new version number +Unless an explicit version number is provided, the patch file name is +analyzed for a sequence of digits separated by dots. If something +like that is found, it is taken to be the new upstream version +number. If not, processing is aborted. +.TP +.B Clean the current source tree +The command \fBdebuild clean\fR is executed within the current Debian +source archive to clean it. If a \fB\-r\fR option is given to +\fBuupdate\fR, it is passed on to \fBdebuild\fR. +.TP +.B Patching +The current source archive (\fI.orig.tar.gz\fR) is unpacked and the +patch applied to the original sources. If this is successful, then +the \fI.orig\fR directory is renamed to reflect the new version number +and the current Debian source directory is copied to a directory with +the new version number, otherwise processing is aborted. The patch is +then applied to the new copy of the Debian source directory. The file +\fIdebian/rules\fR is made executable and all of the \fI.orig\fR files +created by \fBpatch\fR are deleted. If there was a problem with the +patching, a warning is issued and the program will eventually exit +with non-zero exit status. +.TP +.B Changelog update +A changelog entry with the new version number is generated with the +text \*(lqNew upstream release.\*(rq. + +When used on Ubuntu systems, \fBdpkg-vendor\fR detection is used to set +the Debian revision to \*(lq0ubuntu1\*(rq. You may change +\fIdebian/changelog\fR manually afterwards. +.SH "SEE ALSO" +.BR debuild (1), +.BR fakeroot (1), +.BR patch (1), +.BR devscripts.conf (5) + +.B The Debian Policy Manual +.SH AUTHOR +The original version of \fBuupdate\fR was written by Christoph Lameter +<clameter@debian.org>. Several changes and improvements have been +made by Julian Gilbey <jdg@debian.org>. diff --git a/scripts/uupdate.bash_completion b/scripts/uupdate.bash_completion new file mode 100644 index 0000000..7ef6dd9 --- /dev/null +++ b/scripts/uupdate.bash_completion @@ -0,0 +1,47 @@ +# /usr/share/bash-completion/completions/uupdate +# Bash command completion for ‘uupdate(1)’. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +_uupdate() +{ + local cur prev options + + COMPREPLY=() + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + options='--upstream-version -v --rootcmd -r --pristine -u --no-pristine\ + --symlink -s --no-symlink --no-conf --noconf --help -h --version\ + --force-bad-version -b' + + case $prev in + --pristine | -u) + COMPREPLY=( $( + compgen -G "${cur}*[!debian].tar.gz" + compgen -G "${cur}*[!debian].tar.bz2" + compgen -G "${cur}*[!debian].tar.xz" + compgen -G "${cur}*.tgz" + compgen -G "${cur}*.zip" + compgen -G "${cur}*.lzma" ) ) + ;; + --help | -h | --version) + ;; + -*) + COMPREPLY=( $( compgen -W "$options" | grep "^$cur" ) ) + ;; + *) + _filedir + ;; + esac + + return 0 + +} +complete -F _uupdate -o filenames uupdate + + +# Local variables: +# coding: utf-8 +# mode: shell-script +# indent-tabs-mode: nil +# End: +# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 : diff --git a/scripts/uupdate.sh b/scripts/uupdate.sh new file mode 100755 index 0000000..ca6811f --- /dev/null +++ b/scripts/uupdate.sh @@ -0,0 +1,1142 @@ +#!/bin/bash +# +# Upgrade an existing package +# Christoph Lameter, December 24, 1996 +# Many modifications by Julian Gilbey <jdg@debian.org> January 1999 onwards + +# Copyright 1999-2003, Julian Gilbey +# Copyright 2015 Osamu Aoki <osamu@debian.org> (OPMODE=3) +# +# 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/>. + + +# Command line syntax is one of: +# For a new archive: +# uupdate [-v <Version>] [-r <gain-root-command>] [-u] <new upstream archive> +# or +# uupdate [-r <gain-root-command>] [-u] <new upstream archive> <Version> +# or +# uupdate -v <Version> [-r <gain-root-command>] [-n <name>] [-u] -f +# For a patch file: +# uupdate [-v <Version>] [-r <gain-root-command>] -p <patch>.gz +# +# In the first case, the new version number may be specified explicitly, +# either with the -v option before the archive name, or with a version +# number after the archive file name. If both are given, the latter +# takes precedence. +# +# The -u option requests that the new .orig.tar.{gz|bz2} archive be the +# pristine source, although this only makes sense when the original +# archive itself is a tar.gz or tgz archive. +# +# Has to be called from within the source archive + +set -e + +PROGNAME=${0##*/} +MODIFIED_CONF_MSG='Default settings modified by devscripts configuration files:' + +usage() { + echo \ +"Usage for a new archive: + $PROGNAME [options] <new upstream archive> [<version>] +or + $PROGNAME [options] -f|--find +For a patch file: + $PROGNAME [options] --patch|-p <patch>[.gz|.bz2|.lzma|.xz] +Options are: + --no-conf, --noconf + Don't read devscripts config files; + must be the first option given + --upstream-version <version>, -v <version> + specify version number of upstream package + --force-bad-version, -b + Force a version number to be less than the current one + (e.g., when backporting). + --rootcmd <gain-root-command>, -r <gain-root-command> + which command to be used to become root + for package-building + --pristine, -u Source is pristine upstream source and should be + copied to <pkg>_<version>.orig.tar.{gz|bz2|lzma|xz}; + not valid for --patch + --no-symlink Copy new upstream archive to new location as + <pkg>_<version>.orig.tar.{gz|bz2|lzma|xz} instead of + making a symlink; + if it already exists, leave it there as is. + --find, -f Find all upstream tarballs in ../ which match + <pkg>_<version>.orig.tar.{gz|bz2|lzma|xz} or + <pkg>_<version>.orig-<component>.tar.{gz|bz2|lzma|xz} ; + --upstream-version required; pristine source required; + not valid for --patch + --verbose Give verbose output + +$PROGNAME [--help|--version] + show this message or give version information. + +$MODIFIED_CONF_MSG" +} + +version() { + echo \ +"This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +Copyright 1999-2003, Julian Gilbey <jdg@debian.org>, 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." +} + +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 +} + +findzzz() { + LISTNAME=$(ls -1 $@ 2>/dev/null |sed -e 's,\.[^\.]*$,,' | sort | uniq ) + for f in $LISTNAME ; do + if [ -r "$f.xz" ]; then + echo "$f.xz" + elif [ -r "$f.bz2" ]; then + echo "$f.bz2" + elif [ -r "$f.gz" ]; then + echo "$f.gz" + elif [ -r "$f.lzma" ]; then + echo "$f.lzma" + fi + done +} + +# Match Pattern to extract a new version number from a given filename. +# I already had to fiddle with this a couple of times so I better put it up +# at front. It is now written as a Perl regexp to make it nicer. It only +# matches things like: file.3.4 and file2-3.2; it will die on names such +# as file3-2.7a, though. +MPATTERN='^(?:[a-zA-Z][a-zA-Z0-9]*(?:-|_|\.))+(\d+\.(?:\d+\.)*\d+)$' + +STATUS=0 +BADVERSION="" + +# Boilerplate: set config variables +DEFAULT_UUPDATE_ROOTCMD= +DEFAULT_UUPDATE_PRISTINE=yes +DEFAULT_UUPDATE_SYMLINK_ORIG=yes +VARS="UUPDATE_ROOTCMD UUPDATE_PRISTINE UUPDATE_SYMLINK_ORIG" + +SUFFIX="1" +if command -v dpkg-vendor >/dev/null; then + VENDER="$(dpkg-vendor --query Vendor 2>/dev/null|tr 'A-Z' 'a-z')" + case "$VENDER" in + debian) SUFFIX="1" ;; + *) SUFFIX="0${VENDER}1" ;; + esac +fi + +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 '^(UUPDATE|DEVSCRIPTS)_') + + # check sanity + case "$UUPDATE_PRISTINE" in + yes|no) ;; + *) UUPDATE_PRISTINE=yes ;; + esac + + case "$UUPDATE_SYMLINK_ORIG" in + yes|no) ;; + *) UUPDATE_SYMLINK_ORIG=yes ;; + 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 + + +TEMP=$(getopt -s bash -o v:p:r:fubs \ + --long upstream-version:,patch:,rootcmd: \ + --long force-bad-version \ + --long pristine,no-pristine,nopristine \ + --long symlink,no-symlink,nosymlink \ + --long no-conf,noconf \ + --long find \ + --long verbose \ + --long help,version -n "$PROGNAME" -- "$@") || (usage >&2; exit 1) + +eval set -- $TEMP + +OPMODE=2 +# Process Parameters +while [ "$1" ]; do + case $1 in + --force-bad-version|-b) + BADVERSION="-b" ;; + --upstream-version|-v) + shift; NEW_VERSION="$1" ;; + --patch|-p) + shift; PATCH="$1" ; OPMODE=1 ;; + --find|-f) + OPMODE=3 ;; + --rootcmd|-r) + shift; UUPDATE_ROOTCMD="$1" ;; + --pristine|-u) + UUPDATE_PRISTINE=yes ;; + --no-pristine|--nopristine) + UUPDATE_PRISTINE=no ;; + --symlink|-s) + UUPDATE_SYMLINK_ORIG=yes ;; + --no-symlink|--nosymlink) + UUPDATE_SYMLINK_ORIG=no ;; + --no-conf|--noconf) + echo "$PROGNAME: $1 is only acceptable as the first command-line option!" >&2 + exit 1 ;; + --verbose) + UUPDATE_VERBOSE=yes ;; + --help) usage; exit 0 ;; + --version) version; exit 0 ;; + --) shift; break ;; + *) echo "$PROGNAME: bug in option parser, sorry!" >&2 ; exit 1 ;; + esac + shift +done + +if [ "$OPMODE" = 1 ]; then + # --patch mode + if [ $# -ne 0 ]; then + echo "$PROGNAME: additional archive name/version number is not allowed with --patch" >&2 + echo "$PROGNAME: Run $PROGNAME --help for usage information" >&2 + exit 1 + fi +elif [ "$OPMODE" = 2 ]; then + # old "uupdate" used in the version=3 watch file + case $# in + 0) echo "$PROGNAME: no archive given" >&2 ; exit 1 ;; + 1) ARCHIVE="$1" ;; + 2) ARCHIVE="$1"; NEW_VERSION="$2" ;; + *) echo "$PROGNAME: too many non-option arguments" >&2 + echo "$PROGNAME: Run $PROGNAME --help for usage information" >&2 + exit 1 ;; + esac +else + # new "uupdate -f ..." used in the version=4 watch file + if [ $# -ne 0 ]; then + echo "$PROGNAME: additional archive name/version number is not allowed with --component" >&2 + echo "$PROGNAME: Run $PROGNAME --help for usage information" >&2 + exit 1 + fi +fi + +# Get Parameters from current source archive + +if [ ! -f debian/changelog ]; then + echo "$PROGNAME: cannot find debian/changelog." >&2 + echo "$PROGNAME: Are you in the top directory of the source tree?" >&2 + exit 1 +fi + +# Figure out package info we need +mustsetvar PACKAGE "`dpkg-parsechangelog -SSource`" "source package" +mustsetvar VERSION "`dpkg-parsechangelog -SVersion`" "source version" + +# Get epoch and upstream version +eval $(echo "$VERSION" | perl -ne '/^(?:(\d+):)?(.*)/; print "SVERSION=$2\nEPOCH=$1\n";') + +if [ -n "$UUPDATE_VERBOSE" ]; then + if [ "$OPMODE" = 1 ]; then + echo "$PROGNAME: PATCH = \"$PATCH\" is the name of the patch file" >&2 + fi + if [ "$OPMODE" = 2 ]; then + echo "$PROGNAME: ARCHIVE = \"$ARCHIVE\" is the name of the next tarball" >&2 + echo "$PROGNAME: NEW_VERSION = \"$NEW_VERSION\" is the next pristine tarball version" >&2 + fi + echo "$PROGNAME: PACKAGE = \"$PACKAGE\" is in the top of debian/changelog" >&2 + echo "$PROGNAME: VERSION = \"$VERSION\" is in the top of debian/changelog" >&2 + echo "$PROGNAME: EPOCH = \"$EPOCH\" is epoch part of \$VERSION" >&2 + echo "$PROGNAME: SVERSION = \"$SVERSION\" is w/o-epoch part of \$VERSION" >&2 +fi + +UVERSION=$(expr "$SVERSION" : '\(.*\)-[0-9a-zA-Z.+~]*$') +if [ -z "$UVERSION" ]; then + echo "$PROGNAME: a native Debian package cannot take upstream updates" >&2 + exit 1 +fi + +if [ -n "$UUPDATE_VERBOSE" ]; then + echo "$PROGNAME: UVERSION = \"$UVERSION\" the upstream portion w/o-epoch of \$VERSION" >&2 +fi + +# Save pwd before we goes walkabout +OPWD=$(pwd) + +if [ "$OPMODE" = 1 ]; then + # --patch mode + # do the patching + X="${PATCH##*/}" + case "$PATCH" in + /*) + if [ ! -r "$PATCH" ]; then + echo "$PROGNAME: cannot read patch file $PATCH! Aborting." >&2 + exit 1 + fi + case "$PATCH" in + *.gz) CATPATCH="zcat $PATCH"; X=${X%.gz};; + *.bz2) CATPATCH="bzcat $PATCH"; X=${X%.bz2};; + *.lzma) CATPATCH="xz -F lzma -dc $PATCH"; X=${X%.lzma};; + *.xz) CATPATCH="xzcat $PATCH"; X=${X%.xz};; + *) CATPATCH="cat $PATCH";; + esac + ;; + *) + if [ ! -r "$OPWD/$PATCH" -a ! -r "../$PATCH" ]; then + echo "$PROGNAME: cannot read patch file $PATCH! Aborting." >&2 + exit 1 + fi + case "$PATCH" in + *.gz) + if [ -r "$OPWD/$PATCH" ]; then + CATPATCH="zcat $OPWD/$PATCH" + else + CATPATCH="zcat ../$PATCH" + fi + X=${X%.gz} + ;; + *.bz2) + if [ -r "$OPWD/$PATCH" ]; then + CATPATCH="bzcat $OPWD/$PATCH" + else + CATPATCH="bzcat ../$PATCH" + fi + X=${X%.bz2} + ;; + *.lzma) + if [ -r "$OPWD/$PATCH" ]; then + CATPATCH="xz -F lzma -dc $OPWD/$PATCH" + else + CATPATCH="xz -F lzma -dc ../$PATCH" + fi + X=${X%.lzma} + ;; + *.xz) + if [ -r "$OPWD/$PATCH" ]; then + CATPATCH="xzcat $OPWD/$PATCH" + else + CATPATCH="xzcat ../$PATCH" + fi + X=${X%.xz} + ;; + *) + if [ -r "$OPWD/$PATCH" ]; then + CATPATCH="cat $OPWD/$PATCH" + else + CATPATCH="cat ../$PATCH" + fi + ;; + esac + ;; + esac + if [ "$NEW_VERSION" = "" ]; then + # Figure out the new version; we may have to remove a trailing ".diff" + NEW_VERSION=$(echo "$X" | + perl -ne 's/\.diff$//; /'"$MPATTERN"'/ && print $1') + if [ -z "$NEW_VERSION" ]; then + echo "$PROGNAME: new version number not recognized from given filename" >&2 + echo "$PROGNAME: Please run $PROGNAME with the -v option" >&2 + exit 1 + fi + + if [ -n "$EPOCH" ]; then + echo "$PROGNAME: New Release will be $EPOCH:$NEW_VERSION-$SUFFIX." + else + echo "$PROGNAME: New Release will be $NEW_VERSION-$SUFFIX." + fi + fi + + # Strip epoch number + SNEW_VERSION=$(echo "$NEW_VERSION" | perl -pe 's/^\d+://') + if [ $SNEW_VERSION = $NEW_VERSION -a -n "$EPOCH" ]; then + NEW_VERSION="$EPOCH:$NEW_VERSION" + fi + + # Sanity check + if [ -z "$BADVERSION" ] && dpkg --compare-versions "$NEW_VERSION-$SUFFIX" le "$VERSION"; then + echo "$PROGNAME: new version $NEW_VERSION-$SUFFIX <= current version $VERSION; aborting!" >&2 + exit 1 + fi + + if [ -e "../$PACKAGE-$SNEW_VERSION" ]; then + echo "$PROGNAME: $PACKAGE-$SNEW_VERSION already exists in the parent directory!" >&2 + echo "$PROGNAME: Aborting...." >&2 + exit 1 + fi + if [ -e "../$PACKAGE-$SNEW_VERSION.orig" ]; then + echo "$PROGNAME: $PACKAGE-$SNEW_VERSION.orig already exists in the parent directory!" >&2 + echo "$PROGNAME: Aborting...." >&2 + exit 1 + fi + + # Is the old version a .tar.gz or .tar.bz2 file? + if [ -r "../${PACKAGE}_$UVERSION.orig.tar.gz" ]; then + OLDARCHIVE="${PACKAGE}_$UVERSION.orig.tar.gz" + OLDARCHIVETYPE=gz + elif [ -r "../${PACKAGE}_$UVERSION.orig.tar.bz2" ]; then + OLDARCHIVE="${PACKAGE}_$UVERSION.orig.tar.bz2" + OLDARCHIVETYPE=bz2 + elif [ -r "../${PACKAGE}_$UVERSION.orig.tar.lzma" ]; then + OLDARCHIVE="${PACKAGE}_$UVERSION.orig.tar.lzma" + OLDARCHIVETYPE=lzma + elif [ -r "../${PACKAGE}_$UVERSION.orig.tar.xz" ]; then + OLDARCHIVE="${PACKAGE}_$UVERSION.orig.tar.xz" + OLDARCHIVETYPE=xz + else + echo "$PROGNAME: can't find/read ${PACKAGE}_$UVERSION.orig.tar.{gz|bz2|lzma|xz}" >&2 + echo "$PROGNAME: in the parent directory!" >&2 + echo "$PROGNAME: Aborting...." >&2 + exit 1 + fi + + # Clean package + if [ -n "$UUPDATE_ROOTCMD" ]; then + debuild -r"$UUPDATE_ROOTCMD" clean || { + echo "$PROGNAME: couldn't run debuild -r$UUPDATE_ROOTCMD clean successfully." >&2 + echo "$PROGNAME: Aborting...." >&2 + exit 1 + } + else debuild clean || { + echo "$PROGNAME: couldn't run debuild -r$UUPDATE_ROOTCMD clean successfully." >&2 + echo "$PROGNAME: Aborting...." >&2 + exit 1 + } + fi + + cd $(pwd)/.. + rm -rf $PACKAGE-$UVERSION.orig + + # Unpacking .orig.tar.gz is not quite trivial any longer ;-) + TEMP_DIR=$(mktemp -d uupdate.XXXXXXXX) || { + echo "$PROGNAME: can't create temporary directory;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + cd $(pwd)/$TEMP_DIR + if [ "$OLDARCHIVETYPE" = gz ]; then + tar zxf ../$OLDARCHIVE || { + echo "$PROGNAME: can't untar $OLDARCHIVE;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + elif [ "$OLDARCHIVETYPE" = bz2 ]; then + tar --bzip2 -xf ../$OLDARCHIVE || { + echo "$PROGNAME: can't untar $OLDARCHIVE;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + elif [ "$OLDARCHIVETYPE" = lzma ]; then + tar --lzma -xf ../$OLDARCHIVE || { + echo "$PROGNAME: can't untar $OLDARCHIVE;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + elif [ "$OLDARCHIVETYPE" = xz ]; then + tar --xz -xf ../$OLDARCHIVE || { + echo "$PROGNAME: can't untar $OLDARCHIVE;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + else + echo "$PROGNAME: internal error: unknown OLDARCHIVETYPE: $OLDARCHIVETYPE" >&2 + exit 1 + fi + + if [ $(ls | wc -l) -eq 1 ] && [ -d "$(ls)" ]; then + mv "$(ls)" ../${PACKAGE}-$UVERSION.orig + else + mkdir ../$PACKAGE-$UVERSION.orig + mv * ../$PACKAGE-$UVERSION.orig + fi + cd $(pwd)/.. + rm -rf $TEMP_DIR + + cd $(pwd)/$PACKAGE-$UVERSION.orig + if ! $CATPATCH > /dev/null; then + echo "$PROGNAME: can't run $CATPATCH;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + fi + if $CATPATCH | patch -sp1; then + cd $(pwd)/.. + mv $PACKAGE-$UVERSION.orig $PACKAGE-$SNEW_VERSION.orig + echo "-- Originals could be successfully patched" + cp -a $PACKAGE-$UVERSION $PACKAGE-$SNEW_VERSION + cd $(pwd)/$PACKAGE-$SNEW_VERSION + if $CATPATCH | patch -sp1; then + echo "Success. The supplied diffs worked fine on the Debian sources." + else + echo "$PROGNAME: the diffs supplied did not apply cleanly!" >&2 + X=$(find . -name "*.rej" -printf "../$PACKAGE-$SNEW_VERSION/%P\n") + if [ -n "$X" ]; then + echo "Rejected diffs are in $X" >&2 + fi + STATUS=1 + fi + chmod a+x debian/rules + if [ -z "$BADVERSION" ]; then + debchange -v "$NEW_VERSION-$SUFFIX" "New upstream release." + else + debchange $BADVERSION -v "$NEW_VERSION-$SUFFIX" " " + fi + echo "$PROGNAME: Remember: Your current directory is the OLD sourcearchive!" + echo "$PROGNAME: Do a \"cd ../$PACKAGE-$SNEW_VERSION\" to see the new package" + exit + else + echo "$PROGNAME: patch failed to apply to original sources $UVERSION" >&2 + cd $(pwd)/.. + rm -rf $PACKAGE-$UVERSION.orig + exit 1 + fi +elif [ "$OPMODE" = 2 ]; then +# This is an original sourcearchive + # old "uupdate" used in the version=3 watch file + if [ "$ARCHIVE" = "" ]; then + echo "$PROGNAME: upstream source archive not specified" >&2 + exit 1 + fi + case "$ARCHIVE" in + /*) + if [ ! -r "$ARCHIVE" ]; then + echo "$PROGNAME: cannot read archive file $ARCHIVE! Aborting." >&2 + exit 1 + fi + ARCHIVE_PATH="$ARCHIVE" + ;; + *) + if [ "$ARCHIVE" = "../${ARCHIVE#../}" -a -r "$ARCHIVE" ]; then + ARCHIVE_PATH="$ARCHIVE" + elif [ -r "../$ARCHIVE" ]; then + ARCHIVE_PATH="../$ARCHIVE" + elif [ -r "$OPWD/$ARCHIVE" ]; then + ARCHIVE_PATH="$OPWD/$ARCHIVE" + else + echo "$PROGNAME: cannot read archive file $ARCHIVE! Aborting." >&2 + exit 1 + fi + + ;; + esac + + # Figure out the type of archive + X="${ARCHIVE%%/}" + X="${X##*/}" + if [ ! -d "$ARCHIVE_PATH" ]; then + case "$X" in + *.orig.tar.gz) X="${X%.orig.tar.gz}"; UNPACK="tar zxf"; + TYPE=gz ;; + *.orig.tar.bz2) X="${X%.orig.tar.bz2}"; UNPACK="tar --bzip -xf"; + TYPE=bz2 ;; + *.orig.tar.lzma) X="${X%.orig.tar.lzma}"; UNPACK="tar --lzma -xf"; + TYPE=lzma ;; + *.orig.tar.xz) X="${X%.orig.tar.xz}"; UNPACK="tar --xz -xf"; + TYPE=xz ;; + *.tar.gz) X="${X%.tar.gz}"; UNPACK="tar zxf"; TYPE=gz ;; + *.tar.bz2) X="${X%.tar.bz2}"; UNPACK="tar --bzip -xf"; TYPE=bz2 ;; + *.tar.lzma) X="${X%.tar.lzma}"; UNPACK="tar --lzma -xf"; TYPE=lzma ;; + *.tar.xz) X="${X%.tar.xz}"; UNPACK="tar --xz -xf"; TYPE=xz ;; + *.tar.Z) X="${X%.tar.Z}"; UNPACK="tar zxf"; TYPE="" ;; + *.tgz) X="${X%.tgz}"; UNPACK="tar zxf"; TYPE=gz ;; + *.tar) X="${X%.tar}"; UNPACK="tar xf"; TYPE="" ;; + *.zip) X="${X%.zip}"; UNPACK="unzip"; TYPE="" ;; + *.7z) X="${X%.7z}"; UNPACK="7z x"; TYPE="" ;; + *) + echo "$PROGNAME: sorry: Unknown archive type" >&2 + exit 1 + esac + fi + + if [ "$NEW_VERSION" = "" ]; then + # Figure out the new version + NEW_VERSION=$(echo "$X" | perl -ne "/$MPATTERN/"' && print $1') + if [ -z "$NEW_VERSION" ]; then + echo "$PROGNAME: new version number not recognized from given filename" >&2 + echo "$PROGNAME: Please run $PROGNAME with the -v option" >&2 + exit 1 + fi + fi + if [ -n "$EPOCH" ]; then + echo "$PROGNAME: New Release will be $EPOCH:$NEW_VERSION-$SUFFIX." + else + echo "$PROGNAME: New Release will be $NEW_VERSION-$SUFFIX." + fi + + # Strip epoch number + SNEW_VERSION=$(echo "$NEW_VERSION" | perl -pe 's/^\d+://') + if [ $SNEW_VERSION = $NEW_VERSION -a -n "$EPOCH" ]; then + NEW_VERSION="$EPOCH:$NEW_VERSION" + fi + + # Sanity check + if [ -z "$BADVERSION" ] && dpkg --compare-versions "$NEW_VERSION-$SUFFIX" le "$VERSION"; then + echo "$PROGNAME: new version $NEW_VERSION-$SUFFIX <= current version $VERSION; aborting!" >&2 + exit 1 + fi + + if [ -e "../$PACKAGE-$SNEW_VERSION.orig" ]; then + echo "$PROGNAME: original source tree already exists as $PACKAGE-$SNEW_VERSION.orig!" >&2 + echo "$PROGNAME: Aborting...." >&2 + exit 1 + fi + if [ -e "../$PACKAGE-$SNEW_VERSION" ]; then + echo "$PROGNAME: source tree for new version already exists as $PACKAGE-$SNEW_VERSION!" >&2 + echo "$PROGNAME: Aborting...." >&2 + exit 1 + fi + + # Sanity checks + if [ -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.gz" ] && \ + [ "$(md5sum "${ARCHIVE_PATH}" | cut -d" " -f1)" != \ + "$(md5sum "../${PACKAGE}_$SNEW_VERSION.orig.tar.gz" | cut -d" " -f1)" ] + then + echo "$PROGNAME: a different ${PACKAGE}_$SNEW_VERSION.orig.tar.gz" >&2 + echo "$PROGNAME: already exists in the parent dir;" >&2 + echo "$PROGNAME: please check on the situation before trying $PROGNAME again." >&2 + exit 1 + elif [ -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.bz2" ] && \ + [ "$(md5sum "${ARCHIVE_PATH}" | cut -d" " -f1)" != \ + "$(md5sum "../${PACKAGE}_$SNEW_VERSION.orig.tar.bz2" | cut -d" " -f1)" ] + then + echo "$PROGNAME: a different ${PACKAGE}_$SNEW_VERSION.orig.tar.bz2" >&2 + echo "$PROGNAME: already exists in the parent dir;" >&2 + echo "$PROGNAME: please check on the situation before trying $PROGNAME again." >&2 + exit 1 + elif [ -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.lzma" ] && \ + [ "$(md5sum "${ARCHIVE_PATH}" | cut -d" " -f1)" != \ + "$(md5sum "../${PACKAGE}_$SNEW_VERSION.orig.tar.lzma" | cut -d" " -f1)" ] + then + echo "$PROGNAME: a different ${PACKAGE}_$SNEW_VERSION.orig.tar.lzma" >&2 + echo "$PROGNAME: already exists in the parent dir;" >&2 + echo "$PROGNAME: please check on the situation before trying $PROGNAME again." >&2 + exit 1 + elif [ -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.xz" ] && \ + [ "$(md5sum "${ARCHIVE_PATH}" | cut -d" " -f1)" != \ + "$(md5sum "../${PACKAGE}_$SNEW_VERSION.orig.tar.xz" | cut -d" " -f1)" ] + then + echo "$PROGNAME: a different ${PACKAGE}_$SNEW_VERSION.orig.tar.xz" >&2 + echo "$PROGNAME: already exists in the parent dir;" >&2 + echo "$PROGNAME: please check on the situation before trying $PROGNAME again." >&2 + exit 1 + fi + + if [ $UUPDATE_PRISTINE = yes -a -n "$TYPE" -a \ + ! -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.gz" -a \ + ! -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.bz2" -a \ + ! -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.lzma" -a \ + ! -e "../${PACKAGE}_$SNEW_VERSION.orig.tar.xz" ]; then + if [ "$UUPDATE_SYMLINK_ORIG" = yes ]; then + echo "Symlinking to pristine source from ${PACKAGE}_$SNEW_VERSION.orig.tar.$TYPE..." + case $ARCHIVE_PATH in + /*) LINKARCHIVE="$ARCHIVE" ;; + ../*) LINKARCHIVE="${ARCHIVE#../}" ;; + esac + else + echo "$PROGNAME: Copying pristine source to ${PACKAGE}_$SNEW_VERSION.orig.tar.$TYPE..." + fi + + case "$TYPE" in + gz) + if [ "$UUPDATE_SYMLINK_ORIG" = yes ]; then + ln -s "$LINKARCHIVE" "../${PACKAGE}_$SNEW_VERSION.orig.tar.gz" + else + cp "$ARCHIVE_PATH" "../${PACKAGE}_$SNEW_VERSION.orig.tar.gz" + fi + ;; + bz2) + if [ "$UUPDATE_SYMLINK_ORIG" = yes ]; then + ln -s "$LINKARCHIVE" "../${PACKAGE}_$SNEW_VERSION.orig.tar.bz2" + else + cp "$ARCHIVE_PATH" "../${PACKAGE}_$SNEW_VERSION.orig.tar.bz2" + fi + ;; + lzma) + if [ "$UUPDATE_SYMLINK_ORIG" = yes ]; then + ln -s "$LINKARCHIVE" "../${PACKAGE}_$SNEW_VERSION.orig.tar.lzma" + else + cp "$ARCHIVE_PATH" "../${PACKAGE}_$SNEW_VERSION.orig.tar.lzma" + fi + ;; + xz) + if [ "$UUPDATE_SYMLINK_ORIG" = yes ]; then + ln -s "$LINKARCHIVE" "../${PACKAGE}_$SNEW_VERSION.orig.tar.xz" + else + cp "$ARCHIVE_PATH" "../${PACKAGE}_$SNEW_VERSION.orig.tar.xz" + fi + ;; + *) + echo "$PROGNAME: can't preserve pristine sources from non .tar.{gz|bz2|lzma|xz} upstream archive!" >&2 + echo "$PROGNAME: Continuing anyway..." >&2 + ;; + esac + fi + + cd $(pwd)/.. + TEMP_DIR=$(mktemp -d uupdate.XXXXXXXX) || { + echo "$PROGNAME: can't create temporary directory;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + cd $(pwd)/$TEMP_DIR + if [ ! -d "$ARCHIVE_PATH" ]; then + echo "$PROGNAME: Untarring the new sourcecode archive $ARCHIVE" + $UNPACK "$ARCHIVE_PATH" || { + echo "$PROGNAME: can't unpack: $UNPACK $ARCHIVE_PATH failed;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + else + tar -C "$ARCHIVE_PATH/../" -c $X | tar x || { + echo "$PROGNAME: tar -C \"$ARCHIVE_PATH/../\" -c $X | tar x failed;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + } + fi + + cd $(pwd)/.. + if [ $(ls $TEMP_DIR | wc -l) -eq 1 ]; then + # The files are stored in the archive under a top directory, we presume + mv $TEMP_DIR/* $PACKAGE-$SNEW_VERSION + else + # Otherwise, we put them into a new directory + mkdir $PACKAGE-$SNEW_VERSION + mv $TEMP_DIR/* $PACKAGE-$SNEW_VERSION + if ls $TEMP_DIR/.[!.]* > /dev/null 2>&1; then + mv $TEMP_DIR/.[!.]* $PACKAGE-$SNEW_VERSION + fi + fi + rm -rf $TEMP_DIR + cp -a $PACKAGE-$SNEW_VERSION $PACKAGE-$SNEW_VERSION.orig + cd $(pwd)/$PACKAGE-$SNEW_VERSION + + if [ -r "../${PACKAGE}_$SVERSION.diff.gz" ]; then + DIFF="../${PACKAGE}_$SVERSION.diff.gz" + DIFFTYPE=diff + DIFFCAT=zcat + elif [ -r "../${PACKAGE}_$SVERSION.diff.bz2" ]; then + DIFF="../${PACKAGE}_$SVERSION.diff.bz2" + DIFFTYPE=diff + DIFFCAT=bzcat + elif [ -r "../${PACKAGE}_$SVERSION.diff.lzma" ]; then + DIFF="../${PACKAGE}_$SVERSION.diff.lzma" + DIFFTYPE=diff + DIFFCAT="xz -F lzma -dc" + elif [ -r "../${PACKAGE}_$SVERSION.diff.xz" ]; then + DIFF="../${PACKAGE}_$SVERSION.diff.xz" + DIFFTYPE=diff + DIFFCAT=xzcat + elif [ -r "../${PACKAGE}_$SVERSION.debian.tar.gz" ]; then + DIFF="../${PACKAGE}_$SVERSION.debian.tar.gz" + DIFFTYPE=tar + DIFFUNPACK="tar zxf" + elif [ -r "../${PACKAGE}_$SVERSION.debian.tar.bz2" ]; then + DIFF="../${PACKAGE}_$SVERSION.debian.tar.bz2" + DIFFTYPE=tar + DIFFUNPACK="tar --bzip2 -xf" + elif [ -r "../${PACKAGE}_$SVERSION.debian.tar.lzma" ]; then + DIFF="../${PACKAGE}_$SVERSION.debian.tar.lzma" + DIFFTYPE=tar + DIFFUNPACK="tar --lzma -xf" + elif [ -r "../${PACKAGE}_$SVERSION.debian.tar.xz" ]; then + DIFF="../${PACKAGE}_$SVERSION.debian.tar.xz" + DIFFTYPE=tar + DIFFUNPACK="tar --xz -xf" + else + # non-native package and missing diff.gz/debian.tar.xz. + cd $OPWD + if [ ! -d debian ]; then + echo "$PROGNAME: None of *.diff.gz, *.debian.tar.xz, or debian/* found. failed;" >&2 + echo "$PROGNAME: aborting..." >&2 + exit 1 + fi + if [ -d debian/source -a -r debian/source/format ]; then + if [ "$(cat debian/source/format)" = "3.0 (quilt)" ]; then + # This is convenience for VCS users. + echo "$PROGNAME: debian/source/format is \"3.0 (quilt)\"." >&2 + echo "$PROGNAME: Auto-generating ${PACKAGE}_$SVERSION.debian.tar.xz" >&2 + tar --xz -cf ../${PACKAGE}_$SVERSION.debian.tar.xz debian + DIFF="../${PACKAGE}_$SVERSION.debian.tar.xz" + DIFFTYPE=tar + DIFFUNPACK="tar --xz -xf" + else + echo "$PROGNAME: debian/source/format isn't \"3.0 (quilt)\"." >&2 + echo "$PROGNAME: Skip auto-generating ${PACKAGE}_$SVERSION.debian.tar.xz" >&2 + fi + else + echo "$PROGNAME: debian/source/format is missing." >&2 + echo "$PROGNAME: Skip auto-generating ${PACKAGE}_$SVERSION.debian.tar.xz" >&2 + fi + # return back to upstream source + cd $(pwd)/../$PACKAGE-$SNEW_VERSION + fi + + if [ "$DIFFTYPE" = diff ]; then + # Check that any files added in diff do not now exist in + # upstream version + FILES=$($DIFFCAT $DIFF | + perl -nwe 'BEGIN { $status=""; } + chomp; + if (/^--- /) { $status = "-$."; } + if (/^\+\+\+ (.*)/ and $status eq ("-" . ($.-1))) { + $file = $1; + $file =~ s%^[^/]+/%%; + $status = "+$."; + } + if (/^@@ -([^ ]+) /) { + if ($1 eq "0,0" and $status eq ("+" . ($.-1))) { + print "$file\n"; + } + }') + + # Note that debian/changelog is usually in FILES, so FILES is + # usually non-null; however, if the upstream ships its own debian/ + # directory, this may not be true, so must check for empty $FILES. + # Check anyway, even though it's not strictly necessary in bash. + if [ -n "$FILES" ]; then + for file in $FILES; do + if [ -e "$file" ]; then + echo "$PROGNAME warning: file $file was added in old diff, but is now in the upstream source." >&2 + echo "$PROGNAME: Please check that the diff is applied correctly." >&2 + echo "$PROGNAME: (This program will use the pristine upstream version and save the old .diff.gz" >&2 + echo "$PROGNAME: version as $file.debdiff .)" >&2 + + if [ -e "$file.upstream" -o -e "$file.debdiff" ]; then + FILEEXISTERR=1 + fi + fi + done + + if [ -n "$FILEEXISTERR" ]; then + echo "$PROGNAME: please apply the diff by hand and take care with this." >&2 + exit 1 + fi + + # Shift any files that are in the upstream tarball that are also in + # the old diff out of the way so the diff is more likely to apply + # cleanly, and remember the fact that we moved it + for file in $FILES; do + if [ -e "$file" ]; then + mv $file $file.upstream + MOVEDFILES=("${MOVEDFILES[@]}" "$file") + fi + done + fi + + # Remove all existing symlinks before applying the patch. We'll + # restore them afterwards, but this avoids patch following symlinks, + # which may point outside of the source tree + declare -a LINKS + while IFS= read -d '' -r link; do + LINKS+=("$link") + done < <(find -type l -printf '%l\0%p\0' -delete) + + if $DIFFCAT $DIFF | patch -sNp1 ; then + echo "$PROGNAME: Success! The diffs from version $VERSION worked fine." + else + echo "$PROGNAME: the diffs from version $VERSION did not apply cleanly!" >&2 + X=$(find . -name "*.rej") + if [ -n "$X" ]; then + echo "$PROGNAME: Rejected diffs are in $X" >&2 + fi + STATUS=1 + fi + + # Reinstate symlinks, warning for any which fail + for (( i=0; $i < ${#LINKS[@]}; i=$(($i+2)) )); do + target="${LINKS[$i]}" + link="${LINKS[$(($i+1))]}" + if ! ln -s -T "$target" "$link"; then + echo "$PROGNAME: warning: Unable to restore the '$link' -> '$target' symlink." >&2 + STATUS=1 + fi + done + + for file in "${MOVEDFILES[@]}"; do + if [ -e "$file.upstream" ]; then + mv $file $file.debdiff + mv $file.upstream $file + fi + done + + elif [ "$DIFFTYPE" = tar ]; then + if [ -d debian ]; then + echo "$PROGNAME warning: using a debian.tar.{gz|bz2|lzma|xz} file in old Debian source," >&2 + echo "$PROGNAME: but upstream also contains a debian/ directory!" >&2 + if [ -e "debian.upstream" ]; then + echo "$PROGNAME: Please apply the diff by hand and take care with this." >&2 + exit 1 + fi + echo "$PROGNAME: This program will move the upstream directory out of the way" >&2 + echo "$PROGNAME: to debian.upstream/ and use the Debian version" >&2 + mv debian debian.upstream + fi + if [ -n "$UUPDATE_VERBOSE" ]; then + echo "$PROGNAME: Use ${DIFF} to create the new debian/ directory." >&2 + fi + if $DIFFUNPACK $DIFF; then + echo "$PROGNAME: Unpacking the debian/ directory from version $VERSION worked fine." + else + echo "$PROGNAME: failed to unpack the debian/ directory from version $VERSION!" >&2 + exit 1 + fi + else + echo "$PROGNAME: could not find {diff|debian.tar}.{gz|bz2|lzma|xz} from version $VERSION to apply!" >&2 + exit 1 + fi + if [ -f debian/rules ]; then + chmod a+x debian/rules + fi + if [ -n "$UUPDATE_VERBOSE" ]; then + echo "$PROGNAME: New upstream release=$NEW_VERSION-$SUFFIX" >&2 + fi + [ -e ../${PACKAGE}_${NEW_VERSION}.uscan.log ] && \ + cp -f ../${PACKAGE}_${NEW_VERSION}.uscan.log debian/uscan.log + if [ -z "$BADVERSION" ]; then + debchange -v "$NEW_VERSION-$SUFFIX" "New upstream release." + else + debchange $BADVERSION -v "$NEW_VERSION-$SUFFIX" " " + fi + echo "$PROGNAME: Remember: Your current directory is the OLD sourcearchive!" + echo "$PROGNAME: Do a \"cd ../$PACKAGE-$SNEW_VERSION\" to see the new package" + +else + # OPMODE=3: new "uupdate -f ..." used in the version=4 watch file + + # Sanity checks + if [ ! -d debian ]; then + echo "$PROGNAME: cannot find debian/ directory." >&2 + echo "$PROGNAME: Are you in the debianized source tree?" >&2 + echo "$PROGNAME: You may wish to run debmake or dh_make first." >&2 + exit 1 + fi + + if [ ! -x debian/rules ]; then + echo "$PROGNAME: cannot find debian/rules." >&2 + echo "Are you in the top directory of the old source tree?" >&2 + exit 1 + fi + + if [ ! -f debian/changelog ]; then + echo "$PROGNAME: cannot find debian/changelog." >&2 + echo "$PROGNAME: Are you in the top directory of the old source tree?" >&2 + exit 1 + fi + + # Get Parameters from the old source tree + + if [ -e debian/source -a -e debian/source/format ]; then + FORMAT=$(cat debian/source/format) + else + FORMAT='1.0' + fi + + PACKAGE="`dpkg-parsechangelog -SSource`" + if [ -z "$PACKAGE" ]; then + echo "$PROGNAME: cannot find the source package name in debian/changelog." >&2 + exit 1 + fi + + # Variable names follow the convention of old uupdate + VERSION="`dpkg-parsechangelog -SVersion`" + if [ -z "$VERSION" ]; then + echo "$PROGNAME: cannot find the source version name in debian/changelog." >&2 + exit 1 + fi + + EPOCH="${VERSION%:*}" + if [ "$EPOCH" = "$VERSION" ]; then + EPOCH="" + else + EPOCH="$EPOCH:" + fi + SVERSION="${VERSION#*:}" + UVERSION="${SVERSION%-*}" + if [ "$UVERSION" = "$SVERSION" ]; then + echo "$PROGNAME: a native Debian package cannot take upstream updates" >&2 + exit 1 + fi + + if [ -n "$UUPDATE_VERBOSE" ]; then + echo "$PROGNAME: Old: <epoch:><version>-<revision> = $VERSION" + echo "$PROGNAME: Old: <epoch:> = $EPOCH" + echo "$PROGNAME: Old: <version>-<revision> = $SVERSION" + echo "$PROGNAME: Old: <version> = $UVERSION" + echo "$PROGNAME: New: <version> = $NEW_VERSION" + fi + + if [ "$(readlink -f ../${PACKAGE}-$NEW_VERSION)" = "$OPWD" ]; then + echo "$PROGNAME: You can not execute this from ../${PACKAGE}-${NEW_VERSION}/." >&2 + exit 1 + fi + + if [ -e "../${PACKAGE}-$NEW_VERSION" ];then + echo "$PROGNAME: ../${PACKAGE}-$NEW_VERSION directory exists." >&2 + echo "$PROGNAME: remove ../${PACKAGE}-$NEW_VERSION directory." >&2 + rm -rf ../${PACKAGE}-$NEW_VERSION + fi + + # Move to the archive directory + cd $(pwd)/.. + ARCHIVE=$(findzzz ${PACKAGE}_$NEW_VERSION.orig.tar.*z*) + if [ "$FORMAT" = "1.0" ]; then + DEBIANFILE=$(findzzz ${PACKAGE}_$VERSION.debian.diff.*z*) + else + DEBIANFILE=$(findzzz ${PACKAGE}_$VERSION.debian.tar.*z*) + fi + # non-native package and missing diff.gz/debian.tar.xz. + cd $OPWD + if [ -z "$DEBIANFILE" ]; then + if [ -d debian/source -a -r debian/source/format ]; then + if [ "$(cat debian/source/format)" = "3.0 (quilt)" ]; then + # This is convenience for VCS users. + echo "$PROGNAME: debian/source/format is \"3.0 (quilt)\"." >&2 + echo "$PROGNAME: Auto-generating ${PACKAGE}_$SVERSION.debian.tar.xz" >&2 + DEBIANFILE="${PACKAGE}_$SVERSION.debian.tar.xz" + tar --xz -cf ../$DEBIANFILE debian + else + echo "$PROGNAME: debian/source/format isn't \"3.0 (quilt)\"." >&2 + echo "$PROGNAME: Skip auto-generating ${PACKAGE}_$SVERSION.debian.tar.xz" >&2 + exit 1 + fi + else + echo "$PROGNAME: debian/source/format is missing." >&2 + echo "$PROGNAME: Skip auto-generating ${PACKAGE}_$SVERSION.debian.tar.xz" >&2 + exit 1 + fi + fi + # Move to the archive directory + cd $(pwd)/.. + if [ "$FORMAT" = "1.0" ]; then + COMP=${DEBIANFILE##*.} + NEW_DEBIANFILE="${PACKAGE}_${NEW_VERSION}-$SUFFIX.diff.$COMP" + else + COMP=${DEBIANFILE##*.} + NEW_DEBIANFILE="${PACKAGE}_${NEW_VERSION}-$SUFFIX.debian.tar.$COMP" + fi + if [ -e ${NEW_DEBIANFILE} ]; then + if [ "$DEBIANFILE" = "${NEW_DEBIANFILE}" ]; then + echo "$PROGNAME: -> Use existing ${NEW_DEBIANFILE}" >&2 + else + echo "$PROGNAME: -> Overwrite to ${NEW_DEBIANFILE}" >&2 + cp -f $DEBIANFILE ${NEW_DEBIANFILE} + fi + else + echo "$PROGNAME: -> Copy to ${NEW_DEBIANFILE}" >&2 + cp $DEBIANFILE ${NEW_DEBIANFILE} + fi + + # fake DSC + FAKEDSC="${PACKAGE}_${NEW_VERSION}-$SUFFIX.dsc" + echo "Format: ${FORMAT}" > "$FAKEDSC" + echo "Source: ${PACKAGE}" >> "$FAKEDSC" + echo "Version: $EPOCH${NEW_VERSION}-$SUFFIX" >> "$FAKEDSC" + echo "Files:" >> "$FAKEDSC" + if [ -n "$ARCHIVE" ]; then + echo " 01234567890123456789012345678901 1 ${ARCHIVE}" >> "$FAKEDSC" + DPKGOPT="" + elif [ "$FORMAT" = "1.0" ]; then + echo "$PROGNAME: dpkg format \"1.0\" requires the main upstream tarball." >&2 + exit 1 + else + ARCHIVE="${PACKAGE}_${NEW_VERSION}.orig.tar.gz" + mkdir -p ${PACKAGE}-${NEW_VERSION} + tar -czf ${ARCHIVE} ${PACKAGE}-${NEW_VERSION} + rm -rf ${PACKAGE}-${NEW_VERSION} + echo " 01234567890123456789012345678901 1 ${ARCHIVE}" >> "$FAKEDSC" + fi + for f in $(findzzz ${PACKAGE}_${NEW_VERSION}.orig-*.tar.*z*) ; do + echo " 01234567890123456789012345678901 1 $f" >> "$FAKEDSC" + done + echo " 01234567890123456789012345678901 1 ${NEW_DEBIANFILE}" >> "$FAKEDSC" + + # unpack source tree + if ! dpkg-source --skip-patches --no-copy --no-check -x "$FAKEDSC"; then + echo "$PROGNAME: Error with \"dpkg-source --no-copy --no-check -x $FAKEDSC\"" >&2 + echo "$PROGNAME: Remember: Your current directory is changed back to the old source tree!" + echo "$PROGNAME: Do a \"cd ..\" to see $FAKEDSC." + exit 1 + fi + # remove bogus DSC and debian.tar files (generate them with dpkg-source -b) + if [ -z "$UUPDATE_VERBOSE" ]; then + rm -f $FAKEDSC ${NEW_DEBIANFILE} + fi + + # Move to the new source directory + if [ ! -d ${PACKAGE}-${NEW_VERSION} ]; then + echo "$PROGNAME warning: ${PACKAGE}-${NEW_VERSION} directory missing." >&2 + ls -l >&2 + exit 1 + fi + cd $(pwd)/${PACKAGE}-${NEW_VERSION} + [ ! -d debian ] && echo "$PROGNAME: debian directory missing." >&2 && exit 1 + # Need to set permission for format=1.0 + [ -e debian/rules ] && chmod a+x debian/rules + [ -e ../${PACKAGE}_${NEW_VERSION}.uscan.log ] && \ + cp -f ../${PACKAGE}_${NEW_VERSION}.uscan.log debian/uscan.log + if [ -z "$BADVERSION" ]; then + debchange -v "$EPOCH$NEW_VERSION-$SUFFIX" "New upstream release." + else + debchange $BADVERSION -v "$EPOCH$NEW_VERSION-$SUFFIX" " " + fi + echo "$PROGNAME: Remember: Your current directory is changed back to the old source tree!" + echo "$PROGNAME: Do a \"cd ../$PACKAGE-$NEW_VERSION\" to see the new source tree and + edit it to be nice Debianized source." +fi + +if [ $STATUS -ne 0 ]; then + echo "$PROGNAME: (Did you see the warnings above?)" >&2 +fi + +exit $STATUS diff --git a/scripts/what-patch.bash_completion b/scripts/what-patch.bash_completion new file mode 100644 index 0000000..6de9674 --- /dev/null +++ b/scripts/what-patch.bash_completion @@ -0,0 +1,13 @@ +# /usr/share/bash-completion/completions/what-patch +# Bash command completion for ‘what-patch(1)’. +# Documentation: ‘bash(1)’, section “Programmable Completion”. + +complete -W '-v' -o filenames -d what-patch + + +# Local variables: +# coding: utf-8 +# mode: shell-script +# indent-tabs-mode: nil +# End: +# vim: fileencoding=utf-8 filetype=sh expandtab shiftwidth=4 : diff --git a/scripts/what-patch.sh b/scripts/what-patch.sh new file mode 100755 index 0000000..697f05a --- /dev/null +++ b/scripts/what-patch.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# +# Copyright 2006-2008 (C) Kees Cook <kees@ubuntu.com> +# Modified by Siegfried-A. Gevatter <rainct@ubuntu.com> +# Modified by Daniel Hahler <ubuntu@thequod.de> +# +# ################################################################## +# +# 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 for more details. +# +# ################################################################## +# +# By default only the name of the patch system is printed. Verbose mode can be +# enabled with -v. + +if [ "$1" = "-h" ] || [ "$1" = "--help" ] +then + cat <<EOM +Usage: $0 [-v] + +Run this inside the source directory of a Debian package and it will detect +the patch system that it uses. + + -v: Enable verbose mode: + - Print a list of all those files outside the debian/ directory that have + been modified (if any). + - Report additional details about patch systems, if available. +EOM + exit 0 +fi + +while [ ! -r debian/rules ]; +do + if [ "$PWD" = "/" ]; then + echo "Can't find debian/rules." + exit 1 + fi + cd .. +done + +VERBOSE=0 +if [ "$1" = "-v" ] +then + VERBOSE=1 +fi + +if [ "$VERBOSE" -gt 0 ]; then + files=$(lsdiff -z ../$(dpkg-parsechangelog -SSource)_$(dpkg-parsechangelog -SVersion).diff.gz 2>/dev/null | grep -v 'debian/') + if [ -n "$files" ] + then + echo "Following files were modified outside of the debian/ directory:" + echo "$files" + echo "--------------------" + echo + echo -n "Patch System: " + fi +fi + +if grep -qF quilt debian/source/format 2>/dev/null; then + echo "quilt" + exit 0 +fi + +# Do not change the output of existing checks by default, as there are build +# tools that rely on the existing output. If changes in reporting is needed, +# please check the "VERBOSE" flag (see below for examples). Feel free +# to add new patchsystem detection and reporting. +for filename in $(echo "debian/rules"; grep ^include debian/rules | grep -vF '$(' | awk '{print $2}') +do + grep -F patchsys.mk "$filename" | grep -q -v "^#" && { + if [ "$VERBOSE" -eq 0 ]; then + echo "cdbs"; exit 0; + else + echo "cdbs (patchsys.mk: see 'cdbs-edit-patch')"; exit 0; + fi + } + grep -F quilt "$filename" | grep -q -v "^#" && { echo "quilt"; exit 0; } + grep -F dbs-build.mk "$filename" | grep -q -v "^#" && { + if [ "$VERBOSE" -eq 0 ]; then + echo "dbs"; exit 0; + else + echo "dbs (see 'dbs-edit-patch')"; exit 0; + fi + } + grep -F dpatch "$filename" | grep -q -v "^#" && { + if [ "$VERBOSE" -eq 0 ]; then + echo "dpatch"; exit 0; + else + echo "dpatch (see 'patch-edit-patch')"; exit 0; + fi + } + grep -F '*.diff' "$filename" | grep -q -v "^#" && { + if [ "$VERBOSE" -eq 0 ]; then + echo "diff splash"; exit 0; + else + echo "diff splash (check debian/rules)"; exit 0; + fi + } +done +[ -d debian/patches ] || { + if [ "$VERBOSE" -eq 0 ]; then + echo "patchless?"; exit 0; + else + echo "patchless? (did not find debian/patches/)"; exit 0; + fi +} +if [ "$VERBOSE" -eq 0 ]; then + echo "unknown patch system" +else + echo "unknown patch system (see debian/patches/ and debian/rules)" +fi diff --git a/scripts/who-permits-upload.pl b/scripts/who-permits-upload.pl new file mode 100755 index 0000000..6f82eed --- /dev/null +++ b/scripts/who-permits-upload.pl @@ -0,0 +1,353 @@ +#!/usr/bin/perl + +# who-permits-upload - Retrieve permissions granted to Debian Maintainers (DM) +# Copyright (C) 2012 Arno Töll <arno@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 strict; +use Dpkg::Control; +use LWP::UserAgent; +use Encode::Locale; +use Encode; +use Getopt::Long; +use constant { + TYPE_PACKAGE => "package", + TYPE_UID => "uid", + TYPE_SPONSOR => "sponsor" +}; +use constant { SPONSOR_FINGERPRINT => 0, SPONSOR_NAME => 1 }; +use List::Util qw(first); + +our $DM_URL = "https://ftp-master.debian.org/dm.txt"; +our $KEYRING + = "/usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-maintainers.gpg"; +our $TYPE = "package"; +our $GPG = first { !system('sh', '-c', "command -v $_ >/dev/null 2>&1") } + qw(gpg2 gpg); +our ($HELP, @ARGUMENTS, @DM_DATA, %GPG_CACHE); + +binmode STDIN, ':encoding(console_in)'; +binmode STDOUT, ':encoding(console_out)'; +binmode STDERR, ':encoding(console_out)'; + +=encoding utf8 + +=head1 NAME + +who-permits-upload - look-up Debian Maintainer access control lists + +=head1 SYNOPSIS + +B<who-permits-upload> [B<-h>] [B<-s> I<keyring>] [B<-d> I<dm_url>] [B<-s> I<search_type>] I<query> [I<query> ...] + +=head1 DESCRIPTION + +B<who-permits-upload> looks up the given Debian Maintainer (DM) upload permissions +from ftp-master.debian.org and parses them in a human readable way. The tool can +search by DM name, sponsor (the person who granted the permission) and by package. + +=head1 OPTIONS + +=over 4 + +=item B<--dmfile=>I<dm_url>, B<-d> I<dm_url> + +Retrieve the DM permission file from the supplied URL. When this option is not +present, the default value I<https://ftp-master.debian.org/dm.txt> is used. + +=item B<--help>, B<-h> + +Display a usage summary and exit. + +=item B<--keyring=>I<keyring>, B<-s> I<keyring> + +Use the supplied GnuPG keyrings to look-up GPG fingerprints from the DM permission +file. When not present, the default Debian Developer and Maintainer keyrings are used +(I</usr/share/keyrings/debian-keyring.gpg> and +I</usr/share/keyrings/debian-maintainers.gpg>, installed by the I<debian-keyring> +package). + +Separate keyrings with a colon ":". + +=item B<--search=>I<search_type>, B<-s> I<search_type> + +Modify the look-up behavior. This influences the +interpretation of the I<query> argument. Supported search types are: + +=over 4 + +=item B<package> + +Search for a source package name. This is also the default when B<--search> is omitted. +Since package names are unique, this will return given ACLs - if any - for a +single package. + +=item B<uid> + +Search for a Debian Maintainer. This should be (a fraction of) a name. It will +return all ACLs assigned to matching maintainers. + +=item B<sponsor> + +Search for a sponsor (i.e. a Debian Developer) who granted DM permissions. This +will return all ACLs given by the supplied developer. + +Note that this is an expensive operation which may take some time. + +=back + +=item I<query> + +A case sensitive argument to be looked up in the ACL permission file. The exact +interpretation of this argument is dependent by the B<--search> argument. + +This argument can be repeated. + +=back + +=head1 EXIT VALUE + +=over 4 + +=item 0Z<> + +Success + +=item 1Z<> + +An error occurred + +=item 2Z<> + +The command line was not understood + +=back + +=head1 EXAMPLES + +=over 4 + +=item who-permits-upload --search=sponsor arno@debian.org + +Search for all DM upload permissions given by the UID "arno@debian.org". Note, +that only primary UIDs will match. + +=item who-permits-upload -s=sponsor "Arno Töll" + +Same as above, but use a full name instead. + +=item who-permits-upload apache2 + +Look up who gave upload permissions for the apache2 source package. + +=item who-permits-upload --search=uid "Paul Tagliamonte" + +Look up all DM upload permissions given to "Paul Tagliamonte". + +=back + +=head1 AUTHOR + +B<who-permits-upload> was written by Arno Töll <arno@debian.org> and is licensed +under the terms of the General Public License (GPL) version 2 or later. + +=head1 SEE ALSO + +B<gpg>(1), B<gpg2>(1), B<who-uploads>(1) + +S<I<https://lists.debian.org/debian-devel-announce/2012/09/msg00008.html>> + +=cut + +GetOptions( + "help|h" => \$HELP, + "keyring|k=s" => \$KEYRING, + "dmfile|d=s" => \$DM_URL, + "search|s=s" => \$TYPE, +); +# pop positionals +@ARGUMENTS = @ARGV; + +$TYPE = lc($TYPE); +if ($TYPE eq 'package') { + $TYPE = TYPE_PACKAGE; +} elsif ($TYPE eq 'uid') { + $TYPE = TYPE_UID; +} elsif ($TYPE eq 'sponsor') { + $TYPE = TYPE_SPONSOR; +} else { + usage(); +} + +if ($HELP) { + usage(); +} + +if (not @ARGUMENTS) { + usage(); +} + +sub usage { + print STDERR ( +"Usage: $0 [-h][-s KEYRING][-d DM_URL][-s SEARCH_TYPE] QUERY [QUERY ...]\n" + ); + print STDERR "Retrieve permissions granted to Debian Maintainers (DM)\n"; + print STDERR "\n"; + print STDERR "-h, --help\n"; + print STDERR "\t\t\tDisplay this usage summary and exit\n"; + print STDERR "-k, --keyring=KEYRING\n"; + print STDERR + "\t\t\tUse the supplied keyring file(s) instead of the default\n"; + print STDERR "\t\t\tkeyring. Separate arguments by a colon (\":\")\n"; + print STDERR "-d, --dmfile=DM_URL\n"; + print STDERR "\t\t\tRetrieve DM permissions from the supplied URL.\n"; + print STDERR "\t\t\tDefault is https://ftp-master.debian.org/dm.txt\n"; + print STDERR "-s, --search=SEARCH_TYPE\n"; + print STDERR "\t\t\tSupplied QUERY arguments are interpreted as:\n"; + print STDERR + "\t\t\tpackage name when SEARCH_TYPE is \"package\" (default)\n"; + print STDERR "\t\t\tDM user name id when SEARCH_TYPE is \"uid\"\n"; + print STDERR "\t\t\tsponsor user id when SEARCH_TYPE is \"sponsor\"\n"; + exit 2; +} + +sub leave { + my $reason = shift; + chomp $reason; + print STDERR "$reason\n"; + exit 1; +} + +sub lookup_fingerprint { + my $fingerprint = shift; + + if (exists $GPG_CACHE{$fingerprint}) { + return $GPG_CACHE{$fingerprint}; + } + + my @gpg_arguments; + foreach my $keyring (split(":", "$KEYRING")) { + if (!-f $keyring) { + leave("Keyring $keyring is not accessible"); + } + push(@gpg_arguments, ("--keyring", $keyring)); + } + push( + @gpg_arguments, + ( + "--no-options", "--no-auto-check-trustdb", + "--no-default-keyring", "--list-key", + "--with-colons", encode(locale => $fingerprint))); + open(CMD, '-|', $GPG, @gpg_arguments) || leave "$GPG: $!\n"; + binmode CMD, ':utf8'; + my $uid; + while (my $l = <CMD>) { + if ($l =~ /^uid/) { + my $id = (split(":", $l))[9]; + if (!defined($uid)) { + $uid = $id; + } + if ($id =~ /debian\.org/) { + $uid = $id; + # Consume the rest of the output to avoid a potential SIGPIPE when closing CMD + my @junk = <CMD>; + last; + } + } + } + close(CMD) + || leave("gpg returned an error looking for $fingerprint: " . ($? >> 8)); + + $GPG_CACHE{$fingerprint} = $uid; + + return $uid; +} + +sub parse_data { + my $raw_data = shift; + my $parser + = Dpkg::Control->new(type => CTRL_UNKNOWN, allow_duplicate => 1); + open(my $fh, '+<:utf8', \$raw_data) + || leave('unable to read dm data: ' . $!); + my @dm_data = (); + + while ($parser->parse($fh)) { + foreach my $package (split(/,/, $parser->{Allow})) { + if ($package =~ m/([a-z0-9\+\-\.]+)\s+\((\w+)\)/s) { + my @package_row = ( + $1, $parser->{Fingerprint}, + $parser->{Uid}, $2, SPONSOR_FINGERPRINT + ); + push(@dm_data, \@package_row); + } + } + } + return @dm_data; +} + +sub find_matching_row { + my $pattern = shift; + my $type = shift; + my @return_rows; + foreach my $package (@DM_DATA) { + # $package is an array ref in the format + # (package, dm_fingerprint, dm_uid, sponsor_fingerprint callback) + push(@return_rows, $package) + if ($type eq TYPE_PACKAGE && $pattern eq $package->[0]); + push(@return_rows, $package) + if ($type eq TYPE_UID && $package->[2] =~ m/$pattern/); + if ($type eq TYPE_SPONSOR) { + # the sponsor function is a key id so far, mark we looked it up + # already + $package->[3] = lookup_fingerprint($package->[3]); + $package->[4] = SPONSOR_NAME; + if ($package->[3] =~ m/$pattern/) { + push(@return_rows, $package); + } + } + } + return @return_rows; +} + +my $http = LWP::UserAgent->new; +$http->timeout(10); +$http->env_proxy; + +my $response = $http->get($DM_URL); +if ($response->is_success) { + @DM_DATA = parse_data($response->content); +} else { + leave "Could not retrieve DM file: $DM_URL Server returned: " + . $response->status_line; +} + +foreach my $argument (@ARGUMENTS) { + $argument = decode(locale => $argument); + my @rows = find_matching_row($argument, $TYPE); + if (not @rows) { + leave("No $TYPE matches $argument"); + } + foreach my $row (@rows) { + # $package is an array ref in the format + # (package, dm_fingerprint, dm_uid, sponsor_fingerprint, sponsor_type_flag) + my $sponsor = $row->[3]; + if ($row->[4] != SPONSOR_NAME) { + $row->[3] = lookup_fingerprint($row->[3]); + } + printf("Package: %s DM: %s Sponsor: %s\n", + $row->[0], $row->[2], $row->[3]); + } +} diff --git a/scripts/who-uploads.1 b/scripts/who-uploads.1 new file mode 100644 index 0000000..431dc43 --- /dev/null +++ b/scripts/who-uploads.1 @@ -0,0 +1,76 @@ +.TH WHO-UPLOADS 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +who-uploads \- identify the uploaders of Debian source packages +.SH SYNOPSIS +\fBwho\-uploads\fR [\fIoptions\fR] \fIsource_package\fR ... +.SH DESCRIPTION +\fBwho\-uploads\fR uses the Package Tracking System (PTS) to identify +the uploaders of the three most recent versions of the given source +packages. Note that the uploaders are identified using their +\fBgpg\fR(1) or \fBgpg2\fR(1) keys; installing a recent version of the +\fIdebian-keyring\fR package should provide most of the required keys. +.PP +Note that because the PTS uses source packages, you must give the +source package names, not the binary package names. +.SH OPTIONS +.TP +\fB\-M\fR, \fB\-\-max\-uploads=\fIN\fR +Specify the maximum number of uploads to display for each package; the +default is 3. Note that you may not see this many uploads if there +are not this many recorded in the PTS. +.TP +\fB\-\-keyring \fIkeyring\fR +Add \fIkeyring\fR to the list of keyrings to be searched for the +uploader's GPG key. +.TP +\fB\-\-no\-default\-keyrings\fR +By default, \fBwho\-uploads\fR uses the three Debian keyrings +\fI/usr/share/keyrings/debian-keyring.gpg\fR, +\fI/usr/share/keyrings/debian-nonupload.gpg\fR, and +\fI/usr/share/keyrings/debian-maintainers.gpg\fR (although this +default can be changed in the configuration file, see below). +Specifying this option means that the default keyrings will not be +examined. The \fB\-\-keyring\fR option overrides this one. +.TP +\fB\-\-date\fR +Show the date of the upload alongside the uploader's details +.TP +.BR \-\-nodate ", " \-\-no\-date +Do not show the date of the upload alongside the uploader's details. +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 +.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 WHOUPLOADS_DATE +Show the date of the upload alongside the uploader's details. By +default, this is "no". +.TP +.B WHOUPLOADS_MAXUPLOADS +The maximum number of uploads to display for each package. By +default, this is 3. +.TP +.B WHOUPLOADS_KEYRINGS +This is a colon-separated list of the default keyrings to be used. By +default, it is the three Debian keyrings +\fI/usr/share/keyrings/debian-keyring.gpg\fR, +\fI/usr/share/keyrings/debian-nonupload.gpg\fR, +and +\fI/usr/share/keyrings/debian-maintainers.gpg\fR. +.SH AUTHOR +The original version of \fBwho-uploads\fR was written by Adeodato Sim\['o] +<dato@net.com.org.es>. The current version is by Julian Gilbey +<jdg@debian.org>. diff --git a/scripts/who-uploads.sh b/scripts/who-uploads.sh new file mode 100755 index 0000000..17edd16 --- /dev/null +++ b/scripts/who-uploads.sh @@ -0,0 +1,278 @@ +#!/bin/bash + +# who-uploads sourcepkg [ sourcepkg ... ] +# Tells you who made the latest uploads of a source package. +# NB: I'm encoded in UTF-8!! + +# Written and copyright 2006 by Julian Gilbey <jdg@debian.org> +# Based on an original script +# copyright 2006 Adeodato Simó <dato@net.com.org.es> +# +# 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 [options] package ... + Display the most recent three uploaders of each package. + Packages should be source packages, not binary packages. + + Options: + -M, --max-uploads=N + Display at most the N most recent uploads (default: 3) + --keyring KEYRING Add KEYRING as a GPG keyring for Debian Developers' + keys in addition to /usr/share/keyrings/debian-keyring.*, + /usr/share/keyrings/debian-maintainers.gpg and + /usr/share/keyrings/debian-nonupload.gpg; + this option may be given multiple times + --no-default-keyrings + Do not use the default keyrings + --no-conf, --noconf + Don't read devscripts config files; + must be the first option given + --date Display the date of the upload + --no-date, --nodate + Don't display the date of the upload (default) + --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 2006 by Julian Gilbey <jdg@debian.org>, +all rights reserved. +Based on original code copyright 2006 Adeodato Simó <dato@net.com.org.es> +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_WHOUPLOADS_KEYRINGS=/usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-maintainers.gpg:/usr/share/keyrings/debian-nonupload.gpg +DEFAULT_WHOUPLOADS_MAXUPLOADS=3 +DEFAULT_WHOUPLOADS_DATE=no +VARS="WHOUPLOADS_KEYRINGS WHOUPLOADS_MAXUPLOADS WHOUPLOADS_DATE" + +GPG=gpg +if ! command -v $GPG > /dev/null; then + echo "$GPG missing" + GPG=gpg2 + if ! command -v $GPG > /dev/null; then + echo "$GPG missing" + exit 1 + fi +fi + +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 "^WHOUPLOADS_") + + # check sanity + if [ "$WHOUPLOADS_MAXUPLOADS" != \ + "$(echo \"$WHOUPLOADS_MAXUPLOADS\" | tr -cd 0-9)" ]; then + WHOUPLOADS_MAXUPLOADS=3 + fi + + WHOUPLOADS_DATE="$(echo "$WHOUPLOADS_DATE" | tr A-Z a-z)" + if [ "$WHOUPLOADS_DATE" != "yes" ] && [ "$WHOUPLOADS_DATE" != "no" ]; then + WHOUPLOADS_DATE=no + fi + + # don't check WHOUPLOADS_KEYRINGS here + + # 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 + +MAXUPLOADS=$WHOUPLOADS_MAXUPLOADS +WANT_DATE=$WHOUPLOADS_DATE + +declare -a GPG_DEFAULT_KEYRINGS +declare -a GPG_KEYRINGS + +# Command-line options +TEMP=$(getopt -s bash -o 'h' \ + --long max-uploads:,keyring:,no-default-keyrings \ + --long no-conf,noconf \ + --long date,nodate,no-date \ + --long help,version \ + --options M: \ + -n "$PROGNAME" -- "$@") || (usage >&2; exit 1) + +eval set -- $TEMP + +# Process Parameters +while [ "$1" ]; do + case $1 in + --max-uploads|-M) + shift + if [ "$1" = "$(echo \"$1\" | tr -cd 0-9)" ]; then + MAXUPLOADS=$1 + fi + ;; + --keyring) + shift + if [ -f "$1" ]; then + GPG_KEYRINGS=("${GPG_KEYRINGS[@]}" "--keyring" "$1") + else + echo "Could not find keyring $1, skipping" >&2 + fi + ;; + --no-default-keyrings) + WHOUPLOADS_KEYRINGS= + ;; + --no-conf|--noconf) + echo "$PROGNAME: $1 is only acceptable as the first command-line option!" >&2 + exit 1 ;; + --date) WANT_DATE=yes ;; + --no-date|--nodate) WANT_DATE=no ;; + --help|-h) usage; exit 0 ;; + --version) version; exit 0 ;; + --) shift; break ;; + *) echo "$PROGNAME: bug in option parser, sorry!" >&2 ; exit 1 ;; + esac + shift +done + +OIFS="$IFS" +IFS=: + +for keyring in $WHOUPLOADS_KEYRINGS; do + if [ -f "$keyring" ]; then + GPG_DEFAULT_KEYRINGS=("${GPG_DEFAULT_KEYRINGS[@]}" "--keyring" "$keyring") + elif [ -n "$keyring" ]; then + echo "Could not find keyring $keyring, skipping it" >&2 + fi +done + +IFS="${OIFS:- }" + +GNUPGHOME=$(mktemp -d) +exit_with_error() { + [ ! -d "$GNUPGHOME" ] || rm -r "$GNUPGHOME" + exit 1 +} +trap exit_with_error HUP INT QUIT PIPE ALRM TERM +export GNUPGHOME + +# Some useful abbreviations for gpg options +GPG_OPTIONS="--no-options --no-auto-check-trustdb --no-default-keyring" +GPG_NO_KEYRING="$GPG_OPTIONS --keyring /dev/null" + +if [ $# -eq 0 ]; then + usage; + exit 1 +fi + +# Now actually get the reports :) + +for package; do + echo "Uploads for $package:" + + prefix=$(echo $package | sed -re 's/^((lib)?.).*$/\1/') + pkgurl="https://packages.qa.debian.org/${prefix}/${package}.html" + baseurl="https://packages.qa.debian.org/${prefix}/" + + # only grab the actual "Accepted" news announcements; hopefully this + # won't pick up many false positives + WGETOPTS="-q -O - --timeout=30 " + count=0 + for news in $(wget $WGETOPTS $pkgurl | + sed -ne 's%^.*<a href="\('$package'/news/[0-9A-Z]*\.html\)">Accepted .*%\1%p'); do + HTML_TEXT=$(wget $WGETOPTS "$baseurl$news") + GPG_TEXT=$(echo "$HTML_TEXT" | + sed -ne 's/^<pre>//; /-----BEGIN PGP SIGNED MESSAGE-----/,/-----END PGP SIGNATURE-----/p') + + test -n "$GPG_TEXT" || continue + + VERSION=$(echo "$GPG_TEXT" | awk '/^Version/ { print $2; exit }') + DISTRO=$(echo "$GPG_TEXT" | awk '/^Distribution/ { print $2; exit }') + if [ "$WANT_DATE" = "yes" ]; then + DATE=$(echo "$HTML_TEXT" | sed -ne 's%<li><em>Date</em>: \(.*\)</li>%\1%p') + fi + + GPG_ID=$(echo "$GPG_TEXT" | LC_ALL=C $GPG $GPG_NO_KEYRING --keyid-format long --verify 2>&1 | + sed -rne 's/.*using [^ ]* key ([0-9A-Z]+).*/\1/p') + + # First try to get the uid@debian.org email + UPLOADER=$($GPG $GPG_OPTIONS \ + "${GPG_DEFAULT_KEYRINGS[@]}" "${GPG_KEYRINGS[@]}" \ + --list-key --with-colons --fixed-list-mode $GPG_ID 2>/dev/null | + awk -F: '/@debian\.org/ { a = $10; exit} END { print a }' ) + + # If $UPLOADER is still null (no @debian.org email found), pull the next email uid + if [ -z "$UPLOADER" ]; then + UPLOADER=$($GPG $GPG_OPTIONS \ + "${GPG_DEFAULT_KEYRINGS[@]}" "${GPG_KEYRINGS[@]}" \ + --list-key --with-colons --fixed-list-mode $GPG_ID 2>/dev/null | + awk -F: '/^uid/ { a = $10; exit} END { print a }' ) + + fi + + # If $UPLOADER is still null (no email found on the key) print the <unrecognized public key> message + if [ -z "$UPLOADER" ]; then UPLOADER="<unrecognised public key ($GPG_ID)>"; fi + + output="$VERSION to $DISTRO: $UPLOADER" + [ "$WANT_DATE" = "yes" ] && output="$output on $DATE" + echo $output | iconv -c -f UTF-8 + + count=$(($count + 1)) + [ $count -eq $MAXUPLOADS ] && break + done + test $# -eq 1 || echo +done + +[ ! -d "$GNUPGHOME" ] || rm -r "$GNUPGHOME" +exit 0 diff --git a/scripts/whodepends.1 b/scripts/whodepends.1 new file mode 100644 index 0000000..71ed791 --- /dev/null +++ b/scripts/whodepends.1 @@ -0,0 +1,20 @@ +.TH WHODEPENDS 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +whodepends \- check which maintainers' packages depend on a package +.SH SYNOPSIS +\fBwhodepends\fP [\fIpackage\fR] [\fIoptions\fR] +.SH DESCRIPTION +\fBwhodepends\fR gives the names, e-mail addresses and the packages they +maintain of all maintainers who have packages depending on the given +package. +.SH OPTIONS +.TP +.B \-\-help\fP, \fB\-h +Show a brief usage message. +.TP +.B \-\-version\fP, \fB\-v +Show version and copyright information. +.SH BUGS +\fBwhodepends\fR is not very efficient. +.SH AUTHOR +\fBwhodepends\fR has been written by Moshe Zadka <moshez@debian.org>. diff --git a/scripts/whodepends.sh b/scripts/whodepends.sh new file mode 100755 index 0000000..e0b6441 --- /dev/null +++ b/scripts/whodepends.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# whodepends - show maintainers a package depends upon +# by Moshe Zadka <moshez@debian.org> and +# modified by Joshua Kwan <joshk@triplehelix.org> +# This script is in the public domain. + +set -e + +PROGNAME=${0##*/} + +usage() { + cat <<EOF +Usage: $PROGNAME [package] [package] ... [options] + Check which maintainers a particular package depends on. + $PROGNAME options: + --source, -s Show source packages instead of binary ones. + --help, -h Show this help screen. + --version Show version and copyright information. +EOF +} + +version() { + cat <<EOF +This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This code is by Moshe Zadka <moshez@debian.org>, and is in the public domain. +EOF +} + +if [ -z "$1" ]; then + usage + exit 1 +fi + +while [ -n "$1" ]; do + case "$1" in + -s | --source) source=true ;; + -h | --help) usage; exit 0 ;; + --version) version; exit 0 ;; + *) + echo "Dependent maintainers for $1:" + for package in $(apt-cache showpkg $1 | sed -n '/Reverse Depends:/,/Dependencies/p' | grep '^ '|sed 's/,.*//'); do + if [ $source ]; then + apt-cache showsrc $package | + awk '/^Maintainer:/ {maint=$0} /^Package:/ {pkg=$0} END {print maint, pkg}' | + sed 's/Maintainer: //;s/Package: //' + else + apt-cache show $package | + awk '/^Maintainer:/ {maint=$0} END {print maint, "'$package'"}' | + sed 's/Maintainer: //' + fi + done | sort -u | awk -F'>' '{ pack[$1]=pack[$1] $2 } END {for (val in pack) print val ">", "(" pack[val] ")"}' | sed 's/( /(/' + echo + ;; + esac + shift +done diff --git a/scripts/wnpp-alert.1 b/scripts/wnpp-alert.1 new file mode 100644 index 0000000..ce2a40f --- /dev/null +++ b/scripts/wnpp-alert.1 @@ -0,0 +1,34 @@ +.TH WNPP-ALERT 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +wnpp-alert \- check for installed packages up for adoption or orphaned +.SH SYNOPSIS +\fBwnpp-alert \fR[\fB\-\-diff\fR] [\fIpackage\fR ...] +.br +\fBwnpp-alert \-\-help\fR|\fB\-\-version\fR +.SH DESCRIPTION +\fBwnpp-alert\fR downloads the lists of packages which have been +orphaned (O), are up for adoption (RFA), or the maintainer has asked +for help (RFH) from the WNPP webpages, and then outputs a list of +packages installed on the system, or matching the listed packages, +which are in those lists. +.PP +Note that WNPP, and therefore \fBwnpp-alert\fR's output, is source +package based. +.SH OPTIONS +.TP +.BR \-\-diff ", " \-d +If the \fI$XDG_CACHE_HOME/devscripts\fP directory exists, compare the output of +\fBwnpp-alert\fR to the previous output (cached in the file +\fIwnpp-diff\fR) and output the differences. +.TP +.BR \-\-help ", " \-h +Show a summary of options. +.TP +.BR \-\-version ", " \-v +Show version and copyright information. +.SH SEE ALSO +https://www.debian.org/devel/wnpp +.SH AUTHOR +\fBwnpp-alert\fR was written by Arthur Korn <arthur@korn.ch> and +modified by Julian Gilbey <jdg@debian.org> for the devscripts +package. It is in the public domain. diff --git a/scripts/wnpp-alert.sh b/scripts/wnpp-alert.sh new file mode 100755 index 0000000..78c7e6f --- /dev/null +++ b/scripts/wnpp-alert.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# wnpp-alert -- check for installed packages which have been orphaned +# or put up for adoption + +# This script is in the PUBLIC DOMAIN. +# Authors: +# Arthur Korn <arthur@korn.ch> + +# Arthur wrote: +# Get a list of packages with bugnumbers. I tried with LDAP, but this +# is _much_ faster. +# And I (Julian) tried it with Perl's LWP, but this is _much_ faster +# (startup time is huge). And even Perl with wget is slower by 50%.... + +set -e + +PROGNAME=${0##*/} +# TODO: Remove use of OLDCACHEDDIR post-Stretch +OLDCACHEDIR=~/.devscripts_cache +OLDCACHEDDIFF="${OLDCACHEDIR}/wnpp-diff" +CACHEDIR=${XDG_CACHE_HOME:-~/.cache} +CACHEDIR=${CACHEDIR%/}/devscripts +CACHEDDIFF="${CACHEDIR}/wnpp-diff" +CURLORWGET="" +GETCOMMAND="" + +usage() { echo \ +"Usage: $PROGNAME [--help|-h|--version|-v|--diff|-d] [package ...] + List all installed (or listed) packages with Request for + Adoption (RFA), Request for Help (RHF), or Orphaned (O) + bugs against them, as determined from the WNPP website. + https://www.debian.org/devel/wnpp" +} + +version() { echo \ +"This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This script is in the PUBLIC DOMAIN. +Authors: Arthur Korn <arthur@korn.ch> +Modifications: Julian Gilbey <jdg@debian.org>" +} + +wnppdiff() { + if [ -f "$OLDCACHEDDIFF" ]; then + mv "$OLDCACHEDDIFF" "$CACHEDDIFF" + fi + if [ ! -f "$CACHEDDIFF" ]; then + # First use + comm -12 $WNPP_PACKAGES $INSTALLED | sed -e 's/\([+.]\)/\\\1/g' | \ + xargs -I{} grep -E '^[A-Z]+ [0-9]+ {} ' $WNPP | \ + tee "$CACHEDDIFF" + else + comm -12 $WNPP_PACKAGES $INSTALLED | sed -e 's/\([+.]\)/\\\1/g' | \ + xargs -I{} grep -E '^[A-Z]+ [0-9]+ {} ' $WNPP > "$WNPP_DIFF" || true + sort -o "$CACHEDDIFF" "$CACHEDDIFF" + sort -o "$WNPP_DIFF" "$WNPP_DIFF" + comm -3 "$CACHEDDIFF" "$WNPP_DIFF" | \ + sed -e 's/\t/\+/g' -e 's/^\([^+]\)/-\1/g' + mv "$WNPP_DIFF" "$CACHEDDIFF" + fi +} + +if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then usage; exit 0; fi +if [ "$1" = "--version" ] || [ "$1" = "-v" ]; then version; exit 0; fi + +if command -v wget > /dev/null; then + CURLORWGET="wget" + GETCOMMAND="wget -q -O" +elif command -v curl > /dev/null; then + CURLORWGET="curl" + GETCOMMAND="curl -qfsL -o" +else + echo "$PROGNAME: need either the wget or curl package installed to run this" >&2 + exit 1 +fi + + +# Let's abandon this directory from now on, these files are so small +# (see bug#309802) +if [ -d "$CACHEDIR" ]; then + rm -f "$CACHEDIR"/orphaned "$CACHEDIR"/rfa_bypackage +fi + +WNPPTMPDIR=$(mktemp --directory --tmpdir wnppalert.XXXXXX) +trap 'rm -rf "$WNPPTMPDIR"' EXIT +cd "$WNPPTMPDIR" + +INSTALLED=installed +WNPP=wnpp +WNPP_PACKAGES=wnpp_packages + +if [ "$1" = "--diff" ] || [ "$1" = "-d" ]; then + shift + WNPP_DIFF=wnpp_diff +fi + +# Here's a really sly sed script. Rather than first grepping for +# matching lines and then processing them, this attempts to sed +# every line; those which succeed execute the 'p' command, those +# which don't skip over it to the label 'd' +WNPPTMP=orphaned +$GETCOMMAND $WNPPTMP https://www.debian.org/devel/wnpp/orphaned || \ + { echo "$PROGNAME: $CURLORWGET https://www.debian.org/devel/wnpp/orphaned failed" >&2; exit 1; } +sed -ne 's/.*<li><a href="https\?:\/\/bugs.debian.org\/\([0-9]*\)">\([^:<]*\)[: ]*\([^<]*\)<\/a>.*/O \1 \2 -- \3/; T d; p; : d' $WNPPTMP > $WNPP + +WNPPTMP=rfa_bypackage +$GETCOMMAND $WNPPTMP https://www.debian.org/devel/wnpp/rfa_bypackage || \ + { echo "$PROGNAME: $CURLORWGET https://www.debian.org/devel/wnpp/rfa_bypackage" >&2; exit 1; } +sed -ne 's/.*<li><a href="https\?:\/\/bugs.debian.org\/\([0-9]*\)">\([^:<]*\)[: ]*\([^<]*\)<\/a>.*/RFA \1 \2 -- \3/; T d; p; : d' $WNPPTMP >> $WNPP + +WNPPTMP=help_requested +$GETCOMMAND $WNPPTMP https://www.debian.org/devel/wnpp/help_requested || \ + { echo "$PROGNAME: $CURLORWGET https://www.debian.org/devel/wnpp/help_requested" >&2; exit 1; } +sed -ne 's/.*<li><a href="https\?:\/\/bugs.debian.org\/\([0-9]*\)">\([^:<]*\)[: ]*\([^<]*\)<\/a>.*/RFH \1 \2 -- \3/; T d; p; : d' $WNPPTMP >> $WNPP + +cut -f3 -d' ' $WNPP | sort > $WNPP_PACKAGES + +# A list of installed files. + +if [ $# -gt 0 ]; then + printf '%s\n' "$@" | sort -u > $INSTALLED +else + dpkg-query -W -f '${Package} ${Status}\n${Source} ${Status}\n' | \ + awk '/^[^ ].*install ok installed/{print $1}' | \ + sort -u \ + > $INSTALLED +fi + +if [ -n "$WNPP_DIFF" ]; then + # This may fail when run from a cronjob (c.f., #309802), so just ignore it + # and carry on. + mkdir -p "$CACHEDIR" >/dev/null 2>&1 || true + if [ -d "$CACHEDIR" ] || [ -d "$OLDCACHEDIR" ]; then + wnppdiff + exit 0 + else + echo "$PROGNAME: Unable to create diff; displaying full output" + fi +fi + +comm -12 $WNPP_PACKAGES $INSTALLED | sed -e 's/\([+.]\)/\\\1/g' | \ +xargs -I{} grep -E '^[A-Z]+ [0-9]+ {} ' $WNPP || true diff --git a/scripts/wnpp-check.1 b/scripts/wnpp-check.1 new file mode 100644 index 0000000..3292099 --- /dev/null +++ b/scripts/wnpp-check.1 @@ -0,0 +1,42 @@ +.TH WNPP-CHECK 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*- +.SH NAME +wnpp-check \- check if a package is being packaged or if this has been requested +.SH SYNOPSIS +\fBwnpp-check\fR [\fB\-\-exact\fR] \fIpackage\fR ... +.br +\fBwnpp-check \-\-help\fR|\fB\-\-version\fR +.SH DESCRIPTION +\fBwnpp-check\fR downloads the lists of packages which are listed as being +packaged (ITPed) or for which a package has been requested (RFPed) from the +WNPP website and lists any packages supplied on the command line which appear +in those lists. +.PP +Note that WNPP, and therefore \fBwnpp-check\fR's output, is source +package based. +.SH OPTIONS +.TP +.BR \-\-exact ", " \-e +Require an exact package name match, rather than the default substring match. +.TP +.BR \-\-help ", " \-h +Show a summary of options. +.TP +.BR \-\-version ", " \-v +Show version and copyright information. +.SH SEE ALSO +https://www.debian.org/devel/wnpp +.SH EXIT STATUS +.TP +0 +None of the packages supplied has an open ITP or RFP +.TP +1 +Either an error occurred or at least one package supplied has an open ITP or +RFP +.SH AUTHOR +\fBwnpp-check\fR was written by David Paleino <d.paleino@gmail.com>; this man +page was written by Adam D. Barratt <adam@adam-barratt.org.uk> for the +devscripts package. +\fBwnpp-check\fR was originally based on \fBwnpp-alert\fR, which was written +by Arthur Korn <arthur@korn.ch> and modified by Julian Gilbey <jdg@debian.org> +for the devscripts package. Both scripts are in the public domain. diff --git a/scripts/wnpp-check.sh b/scripts/wnpp-check.sh new file mode 100755 index 0000000..6409be5 --- /dev/null +++ b/scripts/wnpp-check.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# wnpp-check -- check for software being packaged or requested + +# This script is in the PUBLIC DOMAIN. +# Authors: +# David Paleino <d.paleino@gmail.com> +# +# Adapted from wnpp-alert, by Arthur Korn <arthur@korn.ch> + +set -e + +CURLORWGET="" +GETCOMMAND="" +EXACT=0 +PROGNAME=${0##*/} + +usage() { echo \ +"Usage: $PROGNAME <package name> [...] + -e,--exact Require an exact package name match, + rather than the default substring match. + + -h,--help Show this help message + -v,--version Show a version message + + Check whether a package is listed as being packaged (ITPed) or has an + outstanding request for packaging (RFP) on the WNPP website + https://www.debian.org/devel/wnpp/" +} + +version() { echo \ +"This is $PROGNAME, from the Debian devscripts package, version ###VERSION### +This script is in the PUBLIC DOMAIN. +Authors: David Paleino <d.paleino@gmail.com> +Adapted from wnpp-alert, by Arthur Korn <arthur@korn.ch>, +with modifications by Julian Gilbey <jdg@debian.org>" +} + +TEMP=$(getopt -n "$PROGNAME" -o 'hve' \ + -l 'help,version,exact' \ + -- "$@") || (rc=$?; usage >&2; exit $rc) + +eval set -- "$TEMP" + +while true +do + case "$1" in + -h|--help) usage; exit 0 ;; + -v|--version) version; exit 0 ;; + -e|--exact) EXACT=1 ;; + --) shift; break ;; + esac + shift +done + +if [ -z "$1" ]; then + usage + exit 1 +fi + +PACKAGES="$@" + +if command -v wget > /dev/null; then + CURLORWGET="wget" + GETCOMMAND="wget -q -O" +elif command -v curl >/dev/null; then + CURLORWGET="curl" + GETCOMMAND="curl -qfsL -o" +else + echo "$PROGNAME: need either the wget or curl package installed to run this" >&2 + exit 1 +fi + +WNPP=$(mktemp --tmpdir wnppcheck-wnpp.XXXXXX) +WNPPTMP=$(mktemp --tmpdir wnppcheck-wnpp.XXXXXX) +WNPP_PACKAGES=$(mktemp --tmpdir wnppcheck-wnpp_packages.XXXXXX) +trap 'rm -f "$WNPP" "$WNPPTMP" "$WNPP_PACKAGES"' EXIT + +# Here's a really sly sed script. Rather than first grepping for +# matching lines and then processing them, this attempts to sed +# every line; those which succeed execute the 'p' command, those +# which don't skip over it to the label 'd' + +$GETCOMMAND $WNPPTMP https://www.debian.org/devel/wnpp/being_packaged || \ + { echo "$PROGNAME: $CURLORWGET https://www.debian.org/devel/wnpp/being_packaged failed." >&2; exit 1; } +sed -ne 's/.*<li><a href="https\?:\/\/bugs.debian.org\/\([0-9]*\)">\([^:<]*\)[: ]*\([^<]*\)<\/a>.*/ITP \1 \2 -- \3/; T d; p; : d' $WNPPTMP > $WNPP + +$GETCOMMAND $WNPPTMP https://www.debian.org/devel/wnpp/requested || \ + { echo "$PROGNAME: $CURLORWGET https://www.debian.org/devel/wnpp/requested failed." >&2; exit 1; } +sed -ne 's/.*<li><a href="https\?:\/\/bugs.debian.org\/\([0-9]*\)">\([^:<]*\)[: ]*\([^<]*\)<\/a>.*/RFP \1 \2 -- \3/; T d; p; : d' $WNPPTMP >> $WNPP + +awk -F' ' '{print "("$1" - #"$2") https://bugs.debian.org/"$2" "$3}' $WNPP | sort -k 5 > $WNPP_PACKAGES + +FOUND=0 +for pkg in $PACKAGES +do + if [ $EXACT != 1 ]; then + grep $pkg $WNPP_PACKAGES && FOUND=1 + else + grep " $pkg$" $WNPP_PACKAGES && FOUND=1 + fi +done + +exit $FOUND diff --git a/scripts/wrap-and-sort b/scripts/wrap-and-sort new file mode 100755 index 0000000..c2c9859 --- /dev/null +++ b/scripts/wrap-and-sort @@ -0,0 +1,524 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2010-2018, Benjamin Drung <bdrung@debian.org> +# 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. + +# pylint: disable=invalid-name +# pylint: enable=invalid-name + +import argparse +import glob +import operator +import os +import re +import sys + +from devscripts.control import ( + HAS_FULL_RTS_FORMATTING, + HAS_RTS_PARSER, + Control, + wrap_and_sort_formatter, +) + +try: + from debian._deb822_repro import LIST_COMMA_SEPARATED_INTERPRETATION +except ImportError: + LIST_COMMA_SEPARATED_INTERPRETATION = object() + + +CONTROL_LIST_FIELDS = ( + "Breaks", + "Build-Conflicts", + "Build-Conflicts-Arch", + "Build-Conflicts-Indep", + "Build-Depends", + "Build-Depends-Arch", + "Build-Depends-Indep", + "Built-Using", + "Conflicts", + "Depends", + "Enhances", + "Pre-Depends", + "Provides", + "Recommends", + "Replaces", + "Suggests", + "Xb-Npp-MimeType", +) + +SUPPORTED_FILES = ( + "clean", + "control", + "control*.in", + "copyright", + "copyright.in", + "dirs", + "*.dirs", + "docs", + "*.docs", + "examples", + "*.examples", + "info", + "*.info", + "install", + "*.install", + "links", + "*.links", + "mainscript", + "*.maintscript", + "manpages", + "*.manpages", + "tests/control", +) + + +def erase_and_write(file_ob, data): + """When a file is opened via r+ mode, replaces its content with data""" + file_ob.seek(0) + file_ob.write(data) + file_ob.truncate() + + +class WrapAndSortControl(Control): + def __init__(self, filename, args): + # The Control module supports the RTS parser with python-debian 0.1.43. + # However, the `wrap_and_sort_formatter` requires 0.1.44. The command line + # option check handles the compatibility check for now. + super().__init__(filename, use_rts_parser=args.rts_parser) + self.args = args + self._formatter = None + if args.rts_parser and HAS_FULL_RTS_FORMATTING: + max_line_length = args.max_line_length + self._formatter = wrap_and_sort_formatter( + 1 if args.short_indent else "FIELD_NAME_LENGTH", + trailing_separator=args.trailing_comma, + immediate_empty_line=args.short_indent, + max_line_length_one_liner=0 if args.wrap_always else max_line_length, + ) + + def wrap_and_sort(self): + for paragraph in self.paragraphs: + for field in CONTROL_LIST_FIELDS: + if field in paragraph: + self._wrap_field(paragraph, field, True) + if "Uploaders" in paragraph: + self._wrap_field(paragraph, "Uploaders", False) + if "Architecture" in paragraph: + archs = set(paragraph["Architecture"].split()) + # Sort, with wildcard entries (such as linux-any) first: + archs = sorted(archs, key=lambda x: ("any" not in x, x)) + paragraph["Architecture"] = " ".join(archs) + + if not self.args.sort_binary_packages or self.filename.endswith( + "tests/control" + ): + return + + if self.had_parse_errors: + if not self.args.dry_run: + print( + f"Skipping sorting of binary packages in {self.filename}:" + " It had parse errors or used template language and the" + " sorting could risk changing the semantics the file." + ) + return + + first = self.paragraphs[: 1 + int(self.args.keep_first)] + sortable = self.paragraphs[1 + int(self.args.keep_first) :] + sort_key = operator.itemgetter("Package") + self.paragraphs = first + sorted(sortable, key=sort_key) + + def _wrap_field(self, control, entry, sort): + if self.is_roundtrip_safe: + self._wrap_field_rts(control, entry, sort) + else: + self._wrap_field_deb822(control, entry, sort) + + def _wrap_field_rts(self, control, entry, sort): + view = control.as_interpreted_dict_view(LIST_COMMA_SEPARATED_INTERPRETATION) + with view[entry] as field_content: + seen = set() + for package_ref in field_content.iter_value_references(): + value = package_ref.value + new_value = " | ".join(x.strip() for x in value.split("|")) + if not sort or new_value not in seen: + package_ref.value = new_value + seen.add(new_value) + else: + package_ref.remove() + if sort: + field_content.sort(key=_sort_packages_key) + if self._formatter: + field_content.value_formatter(self._formatter) + field_content.reformat_when_finished() + + def _wrap_field_deb822(self, control, entry, sort): + # An empty element is not explicitly disallowed by Policy but known to + # break QA tools, so remove any + packages = [x.strip() for x in control[entry].split(",") if x.strip()] + + # Sanitize alternative packages. E.g. "a|b |c" -> "a | b | c" + packages = [" | ".join(x.strip() for x in p.split("|")) for p in packages] + + if sort: + # Remove duplicate entries + packages = set(packages) + packages = sort_list(packages) + + length = len(entry) + sum(2 + len(package) for package in packages) + if self.args.wrap_always or length > self.args.max_line_length: + indentation = " " + if not self.args.short_indent: + indentation *= len(entry) + len(": ") + packages_with_indention = [indentation + x for x in packages] + packages_with_indention = ",\n".join(packages_with_indention) + if self.args.trailing_comma: + packages_with_indention += "," + if self.args.short_indent: + control[entry] = "\n" + packages_with_indention + else: + control[entry] = packages_with_indention.strip() + else: + new_value = ", ".join(packages) + if self.args.trailing_comma: + new_value += "," + control[entry] = new_value + + def check_changed(self): + """Checks if the content has changed in the control file""" + content = self.dump() + with open(self.filename, "r", encoding="utf8") as control_file: + if content != control_file.read(): + return True + return False + + +class InstallContent: + __slots__ = ("content", "comments") + + def __init__(self, content, comments=None): + self.content = content + self.comments = comments + + def __str__(self): + comments = "\n".join(self.comments) + "\n" if self.comments else "" + return comments + self.content + + def __eq__(self, other): + return self.content == other.content + + def __lt__(self, other): + return self.content < other.content + + +class Install: + def __init__(self, filename, args): + self.content = None + self.filename = None + self.args = args + self.leading_comments = None + self.trailing_comments = None + self.open(filename) + + def open(self, filename): + assert os.path.isfile(filename), f"{filename} does not exist." + self.filename = filename + comments = [] + content = [] + with open(filename, encoding="utf8") as f: + # When reading a debhelper file, we want to preserve blocks of comments. + # + # For the purpose of wrap-and-sort, we generally associate comments with + # a line of (non-whitespace) content. Though as special cases, we also + # preserve a file starting with a comment (with an empty line before the + # first content to distinguish it from a comment to that comment line) as + # well as trailing comments (i.e. comments before EOF). + for line in f: + line = line.strip() + if not line: + if comments and not content: + self.leading_comments = InstallContent("", comments=comments) + comments = [] + continue + if line[0] == "#": + comments.append(line) + continue + if comments: + content.append(InstallContent(line, comments=comments)) + comments = [] + else: + content.append(InstallContent(line, comments=None)) + self.trailing_comments = comments + self.content = content + + def save(self): + to_write = self._serialize_content() + + with open(self.filename, "r+", encoding="utf8") as install_file: + content = install_file.read() + if to_write != content: + if not self.args.dry_run: + erase_and_write(install_file, to_write) + return True + return False + + def _serialize_content(self): + elements = [] + if self.leading_comments: + elements.append(str(self.leading_comments)) + elements.extend(str(x) for x in self.content) + if self.trailing_comments: + # Add a space between the last content and the trailing comments + # for readability + elements.append("\n" + "\n".join(self.trailing_comments)) + return "\n".join(elements) + "\n" + + def sort(self): + self.content = sorted(self.content) + + +def remove_trailing_whitespaces(filename, args): + assert os.path.isfile(filename), f"{filename} does not exist." + with open(filename, "br+") as file_object: + content = file_object.read() + if not content: + return True + new_content = content.strip() + b"\n" + new_content = b"\n".join([line.rstrip() for line in new_content.split(b"\n")]) + if new_content != content: + if not args.dry_run: + erase_and_write(file_object, new_content) + return True + return False + + +def sort_list(unsorted_list): + return sorted(unsorted_list, key=_sort_packages_key) + + +def _sort_packages_key(package): + # Sort dependencies starting with a "real" package name before ones starting + # with a substvar + return 0 if re.match("[a-z0-9]", package) else 1, package + + +def wrap_and_sort(args): + modified_files = [] + control_files = [f for f in args.files if re.search("/control[^/]*$", f)] + for control_file in control_files: + if args.verbose: + print(control_file) + try: + control = WrapAndSortControl(control_file, args) + except ValueError as e: + print( + f"W: Could not parse {control_file} as a Deb822 file: {str(e.args[0])}", + file=sys.stderr, + ) + continue + if args.cleanup: + control.strip_trailing_whitespace_on_save = True + control.wrap_and_sort() + if control.check_changed(): + if not args.dry_run: + control.save() + modified_files.append(control_file) + + copyright_files = [f for f in args.files if re.search("/copyright[^/]*$", f)] + for copyright_file in copyright_files: + if args.verbose: + print(copyright_file) + if remove_trailing_whitespaces(copyright_file, args): + modified_files.append(copyright_file) + + pattern = "(dirs|docs|examples|info|install|links|maintscript|manpages)$" + install_files = [f for f in args.files if re.search(pattern, f)] + for install_file in sorted(install_files): + if args.verbose: + print(install_file) + install = Install(install_file, args) + install.sort() + if install.save(): + modified_files.append(install_file) + + return modified_files + + +def get_files(debian_directory): + """Returns a list of files that should be wrapped and sorted.""" + files = [] + for supported_files in SUPPORTED_FILES: + file_pattern = os.path.join(debian_directory, supported_files) + files.extend( + file_name + for file_name in glob.glob(file_pattern) + if not os.access(file_name, os.X_OK) + ) + return files + + +def main(): + script_name = os.path.basename(sys.argv[0]) + epilog = f"See {script_name}(1) for more info." + parser = argparse.ArgumentParser(epilog=epilog) + + # Remember to keep doc/wrap-and-sort.1 updated! + parser.add_argument( + "-a", + "--wrap-always", + action="store_true", + default=False, + help="wrap lists even if they do not exceed the line length limit", + ) + parser.add_argument( + "-s", + "--short-indent", + dest="short_indent", + help="only indent wrapped lines by one space" + " (default is in-line with the field name)", + action="store_true", + default=False, + ) + parser.add_argument( + "-b", + "--sort-binary-packages", + help="Sort binary package paragraphs by name", + dest="sort_binary_packages", + action="store_true", + default=False, + ) + parser.add_argument( + "-k", + "--keep-first", + help="When sorting binary package paragraphs, leave the first one at the top." + " Unqualified debhelper configuration files are applied to the first package.", + dest="keep_first", + action="store_true", + default=True, + ) + parser.add_argument( + "--no-keep-first", + help="When sorting binary package paragraphs, do not treat the first" + " binary package specially. This may cause Unqualified debhelper" + " configuration files to be applied to a different package than" + " originally intended", + dest="keep_first", + action="store_false", + default=True, + ) + parser.add_argument( + "-n", + "--no-cleanup", + help="do not remove trailing whitespaces", + dest="cleanup", + action="store_false", + default=True, + ) + parser.add_argument( + "-t", + "--trailing-comma", + help="add trailing comma", + dest="trailing_comma", + action="store_true", + default=False, + ) + parser.add_argument( + "-d", + "--debian-directory", + dest="debian_directory", + help="location of the 'debian' directory (default: ./debian)", + metavar="PATH", + default="debian", + ) + parser.add_argument( + "-f", + "--file", + metavar="FILE", + dest="files", + action="append", + default=[], + help="Wrap and sort only the specified file.", + ) + parser.add_argument( + "-v", + "--verbose", + help="print all files that are touched", + dest="verbose", + action="store_true", + default=False, + ) + parser.add_argument( + "--max-line-length", + type=int, + default=79, + help="set maximum allowed line length before wrapping (default: %(default)i)", + ) + parser.add_argument( + "-N", + "--dry-run", + dest="dry_run", + action="store_true", + default=False, + help="do not modify any file, instead only print the files" + " that would be modified", + ) + parser.add_argument( + "--experimental-rts-parser", + dest="rts_parser", + action="store_true", + default=False, + help="Use the round-safe parser, which can preserve most comments. The option" + " is here to opt-in to using while the feature matures. Some options are not" + " available with this feature. Note this option will eventually be removed." + " Please do not include it in scripts / functionality that requires backwards" + " compatibility", + ) + + args = parser.parse_args() + use_rts_parser = HAS_RTS_PARSER + if use_rts_parser and not HAS_FULL_RTS_FORMATTING: + # Cases where we might have some but not full support. + if args.short_indent or not args.wrap_always or not args.trailing_comma: + use_rts_parser = False + setattr(args, "rts_parser", use_rts_parser) + + if not os.path.isdir(args.debian_directory): + parser.error( + f'Debian directory not found, expecting "{args.debian_directory}".' + ) + + not_found = [f for f in args.files if not os.path.isfile(f)] + if not_found: + parser.error(f"Specified files not found: {', '.join(not_found)}") + + if not args.files: + args.files = get_files(args.debian_directory) + + modified_files = wrap_and_sort(args) + + # Only report at the end, to avoid potential clash with --verbose + if modified_files and (args.verbose or args.dry_run): + if args.dry_run: + header = "--- Dry run, these files would be modified ---" + else: + header = "--- Modified files ---" + print(header) + print("\n".join(modified_files)) + elif args.verbose: + print("--- No file needs modification ---") + + +if __name__ == "__main__": + main() |