#!/usr/bin/bash -e # Copyright © 2018-2019 Nicolas Mailhot , # Jan Chaloupka # # 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 . # # SPDX-License-Identifier: GPL-3.0-or-later usage() { cat >&2 << EOF_USAGE Usage: $0 [ [-h] ] [ [-i ] ] [ [-y] ] [ [-p ] [-g ] ] [ [-w] ] [ [-b ] ] [ [-d ] [-t ] [-r ] ] [ [-e ] [-s ] [-o ] ] [ [-v ] ] [ [-a ] ] should be one of: install, check, provides, requires Most actions accept the same set of arguments, and will silently ignore those that do not apply to a specific action. Unless specified otherwise, all arguments are optional. “install”-specific arguments: -i is a mandatory argument -e add files ending in to the default installation set, can be specified several times -s read expanded and prepared Go sources in /src should be populated in %prep -o output file lists to file, default value if not set: devel.file-list -O create file list in add to the default installation set, can be specified several times “check”-specific arguments: -i is a mandatory argument -y check the files installed in the system Go path, not the files in the current work directory, this option is usually used with -p and -g, -w check the files in the current work directory, not the files installed in the system Go path, this option is usually used with -b, this is the default check mode if neither -y nor -w are specified “provides”-specific arguments: -v : tag the provides with -a : an attribute to add to the provides, for example -a "(commit=XXXX)" -a "(branch=YYYY)" -a "(tag=rx.y.z-alpha1)" can be specified several times Common arguments: -i a Go import path of the target package, mandatory for: install and check, ignored by: provides and requires -h print this help -p : an optionnal prefix path such as %{buildroot} -g : the root of the Go source tree default value if not set: /usr/share/gocode -b read binaries already produced in used by: install and check, ignored by: provides and requires -d a directory that should be ignored during processing, relative to the import path root, non recursive, can be specified several times -t the root of a directory tree that should be ignored during processing, relative to the import path root, recursive, can be specified several times -r a regex matching elements that should be ignored during processing, relative to the import path root, can be specified several times EOF_USAGE exit 1 } multigoipath() { cat >&2 << EOF_MULTIGOIPATH Error: Attempt to install to multiple Go import paths. Aborting… The installation target of “$0 install” is a unique Go import path tree. Some of the presets rpm passes to this command in the %install stage rely on this unicity. If you need to install files to different Go import path trees you MUST invoque this command once per target. Even though the import path value was originally intended to be implicit in Go, it usually leaks in multiple project files (documentation, unit tests, non-Go files and so on). Therefore you SHOULD always: — determine the canonical import path name of the project you want to install, — change every self-reference in the project files, to this canonical name, removing alternative leftovers (usually done, in the %prep stage of rpm) — and only then deploy the result to the canonical import path using “$0 install -i ” You MAY use symbolic links to maintain compatibility with codebases that refer to this project via alternative, non canonical names (due to past renamings or forkings). That will have the same effect as the HTTP redirects some project upstreams are fond of. It WILL break the same way on the explicit self-references that may exist in the files of this project. Therefore, you SHOULD preferably patch those codebases to use the project canonical import path, like you did for this codebase in %prep. If you can not patch away references to an alternative import path name in some third-party codebases, but wish to avoid import path naming problems, you SHOULD: — prepare, in %prep, a separate copy of the project files, with every self reference changed to the alternative name — and only then deploy, from this separate copy, to the alternative name “$0 install -i \ -s ” This later pattern is not recommended as it helps entrenching naming confusion. EOF_MULTIGOIPATH exit 1 } alreadyexists() { cat >&2 << EOF_ALREADYEXISTS Error: Attempt to overwrite an existing installation. Aborting… “$0 install” aims at deploying clean Go source project files in the %install stage of rpm. You SHOULD have prepared the project files in the rpm %prep stage and resolved any file conflict before calling “$0 install”. That means a specific Go import path is usually deployed in a single call. If you wish to deploy the same import path using multiple “$0 install” calls you MUST make sure they use consistent versionning and exclusion arguments. Those arguments are recorded on disk to allow reliable provides computation in later rpm stages. If used outside rpm, you MUST ensure some other way no conflicting installation already exists. “$0 install” has no way to remove cleanly such an installation. Installing blindly over another deployment would leave in place, all the files, not present in the new installation, resulting in an unreliable mix. EOF_ALREADYEXISTS exit 1 } action='' prefix='' checkin="workdir" sourcedir="${PWD}" bindir="${GO_BUILD_DIR:-${PWD}/_build}" goldflags='' gopath=/usr/share/gocode filelist='devel.file-list' filelistroot="${PWD}" goipath='' declare -A metadata flags_d=() flags_t=() flags_r=() flags_e=() declare -A imetadata iflags_d=() iflags_t=() iflags_r=() iflags_e=() vinstall=install vln=ln if [[ $# -eq 0 ]] ; then usage else case $1 in install|check|provides|requires) action=$1 ;; *) usage ;; esac fi shift if ! options=$(getopt -n $0 -o hi:yp:g:wb:d:t:r:e:s:o:O:l:V:T:C:B:vaz: \ -l help,go-import-path: \ -l system-files,prefix:,go-path: \ -l workdir-files,bindir: \ -l ignore-directory: \ -l ignore-tree: \ -l ignore-regex: \ -l include-extension: \ -l sourcedir: \ -l output:,output-root \ -l version:,tag:,commit:,branch: \ -l verbose: \ -- "$@") ; then usage fi eval set -- "$options" while [ $# -gt 0 ] ; do case $1 in -h|--help) usage ;; -i|--go-import-path) if [[ ("${goipath}" != "") && \ ("${goipath}" != "${2}") ]] ; then multigoipath else goipath="${2}" fi ; shift;; -y|--system-files) checkin="system" ;; -p|--prefix) prefix=$(realpath -sm "${2}") ; shift;; -g|--go-path) gopath="$2" ; shift;; -w|--workdir-files) checkin="workdir" ;; -b|--bindir) bindir="${2}" ; shift;; -d|--ignore-directory) iflags_d+=( "${2}" ) ; shift;; -t|--ignore-tree) iflags_t+=( "${2}" ) ; shift;; -r|--ignore-regex) iflags_r+=( "${2}" ) ; shift;; -e|--include-extension) iflags_e+=( "${2}" ) ; shift;; -s|--sourcedir ) sourcedir="${2}" ; shift;; -o|--output) filelist="${2}" ; shift;; -O|--output-root) filelistroot="${2}" ; shift;; -l|--ldflags) ldflags="${2}" ; shift;; -V|--version) imetadata["version"]="${2}" ; shift;; -T|--tag) imetadata["tag"]="${2}" ; shift;; -C|--commit) imetadata["commit"]="${2}" ; shift;; -B|--branch) imetadata["branch"]="${2}" ; shift;; -v|--verbose) vinstall="install -v" vln="ln -v" ;; -a) ;; # ignored, only used by the rpm macro side -z) shift;; # ignored, only used by the rpm macro side (--) shift; break ;; (-*) usage ;; (*) break ;; esac shift done dedupearray() { local -n arrayref="${1}" if [[ "${#arrayref[@]}" != "0" ]] ; then local temparray=( "${arrayref[@]}" ) arrayref=() while read -r -d $'\0' l ; do arrayref+=( "${l}" ) done < <(printf "%s\0" "${temparray[@]}" | sort -z -u) fi } fixuserflags() { if [[ -z "${goipath}" ]] ; then echo "No import path was specified using -i, exiting" exit 1 fi flags_d=( "${flags_d[@]##${goipath}/}" ) flags_d=( "${flags_d[@]/#/${goipath}/}" ) flags_d=( "${flags_d[@]%/.}" ) dedupearray flags_d flags_t=( "${flags_t[@]##${goipath}/}" ) flags_t=( "${flags_t[@]/#/${goipath}/}" ) flags_t=( "${flags_t[@]%/.}" ) dedupearray flags_t dedupearray flags_r dedupearray flags_e } expandflags() { echo ${flags_d[@]/#/ -d } ${flags_t[@]/#/ -t } ${flags_r[@]/#/ -r } ${flags_e[@]/#/ -e } } popmetadata() { unset metadata declare -g -A metadata for k in "${!imetadata[@]}"; do metadata["${k}"]=${imetadata[${k}]} done flags_d=( "${iflags_d[@]}" ) flags_t=( "${iflags_t[@]}" ) flags_r=( "${iflags_r[@]}" ) flags_e=( "${iflags_e[@]}" ) } savemetadata() { medadatafile="${1}" rm -f "${medadatafile}" touch "${medadatafile}" for m in version tag commit branch ; do if [[ -n "${metadata[${m}]}" ]] ; then echo "${m}:${metadata[${m}]}" >> "${medadatafile}" fi done [[ "${#flags_d[@]}" != "0" ]] && printf 'excludedir:%s\n' "${flags_d[@]}" >> "${medadatafile}" || : [[ "${#flags_t[@]}" != "0" ]] && printf 'excludetree:%s\n' "${flags_t[@]}" >> "${medadatafile}" || : [[ "${#flags_r[@]}" != "0" ]] && printf 'excluderegex:%s\n' "${flags_r[@]}" >> "${medadatafile}" || : } readmetadata() { medadatafile="${1}" popmetadata for m in version tag commit branch ; do v=$(grep "^${m}\:" "${medadatafile}" | head -1) v="${v#${m}:}" [[ -n "${v}" ]] && metadata["${m}"]="${v}" done while read -r -d $'\n' l ; do flags_d+=( "${l#excludedir:}" ) done < <(grep "^excludedir:" "${medadatafile}") dedupearray flags_d while read -r -d $'\n' l ; do flags_t+=( "${l#excludetree:}" ) done < <(grep "^excludetree:" "${medadatafile}") dedupearray flags_t while read -r -d $'\n' l ; do flags_r+=( "${l#excluderegex:}" ) done < <(grep "^excluderegex:" "${medadatafile}") dedupearray flags_r } installfile() { local goipath="${1}" local file="${2}" for proot in "$(realpath -sm ${PWD})" \ "${gopath}/src/${goipath}" \ "${prefix}${gopath}/src/${goipath}"; do [[ ${file}/ == ${proot}/* ]] && file=".${file#${proot}}" done file=$(realpath -sm "${file}") local workdir="$(realpath -sm .)" if [[ ${file}/ == ${workdir}/* ]] ; then local dest="${prefix}${gopath}/src/${goipath}${file#${workdir}}" else local dest="${prefix}${file}" fi local destdir=$(dirname "${dest}") if [[ ! -d "${destdir}" ]] ; then installfile "${goipath}" "${destdir#${prefix}}" fi if [[ (! -e $file) || (-d "${file}" && ! -L "${file}") ]] ; then ${vinstall} -m 0755 -d "${dest}" if [[ -d "${file}" && ! -L "${file}" ]] ; then find "${file}" -maxdepth 1 -iname '*.md' -type f -print0 | \ while read -r -d $'\0' docfile ; do installfile "${goipath}" "${docfile}" done fi local fllprefix="%dir" else if [[ -L "${file}" ]] ; then ${vln} -s $(readlink "${file}") "${dest}" touch -h -r "${file}" "${dest}" else if [[ -e "${dest}" ]] ; then checksum=$(sha256sum "${file}") checksum="${checksum%% *}" destchecksum=$(sha256sum "${dest}") destchecksum="${destchecksum%% *}" [[ "${checksum}" != "${destchecksum=}" ]] && alreadyexists fi ${vinstall} -m 0644 -p "${file}" "${dest}" fi [[ "${file}" == *.md ]] && local fllprefix="%doc" fi echo "${fllprefix:+${fllprefix} }\"${dest#${prefix}}\"" >> "${filelist}" } listfiles() { local goipath="${1}" GOPATH="${sourcedir}" GO111MODULE=off \ golist --to-install --package-path ${goipath} $(expandflags) } checks() { local goipath="${1}" GOPATH="${workroot}${GOPATH+:${GOPATH}}" GO111MODULE=off \ golist --provided --with-tests --package-path ${goipath} $(expandflags) |\ while read -r -d $'\n' dir ; do pushd "${workroot}/src/${dir}" >/dev/null echo "${dir}" GOPATH="${workroot}${GOPATH:+:${GOPATH}}:${gopath}" \ PATH="${workbin:+${workbin}:}${PATH}" \ GO111MODULE=off \ go test ${GO_TEST_FLAGS} -ldflags "${LDFLAGS:+${LDFLAGS} }-extldflags '${GO_TEST_EXT_LD_FLAGS}'" popd >/dev/null done } provides() { local goipath="${1}" ( echo "golang-ipath(${goipath})" GOPATH="${prefix}${gopath}" GO111MODULE=off \ golist --provided --package-path "${goipath}" $(expandflags) \ --skip-self --template 'golang({{.}})\n' ) | while IFS= read -r prov ; do echo "${prov}${metadata[version]:+ = ${metadata[version]}}" for m in "${!metadata[@]}" ; do if [[ "${m}" != "version" ]] ; then echo "${prov}(${m}=${metadata[${m}]})${metadata[version]:+ = ${metadata[version]}}" fi done done } requires() { GOPATH="${prefix}${gopath}" GO111MODULE=off \ golist --imported --package-path "${1}" $(expandflags) \ --skip-self --template 'golang({{.}})\n' } # Action-specific preparation case $action in install) popmetadata fixuserflags filelist="${filelistroot}/${filelist}" ${vinstall} -m 0755 -d "${prefix}${gopath}/src" echo "Installing: ${goipath}" pushd "${sourcedir}/src/${goipath}" >/dev/null savemetadata .goipath touch -r "${sourcedir}" .goipath ( listfiles "${goipath}" realpath -e -s --relative-base=. "$@" go.mod .goipath ) | sort -u | while IFS= read -r file ; do installfile "${goipath}" "${file}" done rm .goipath popd >/dev/null sort -u -o "${filelist}" "${filelist}" ;; check) popmetadata fixuserflags if [[ ${checkin} == "system" ]] ; then workroot="${prefix}${gopath}" unset workbin else workroot="${sourcedir}" workbin="${bindir}" fi echo "Testing in: ${workroot}/src" echo " PATH: ${workbin:+${workbin}:}${PATH}" echo " GOPATH: ${workroot}${GOPATH:+:${GOPATH}}:${gopath}" echo " GO111MODULE: off" echo " command: go test ${GO_TEST_FLAGS} -ldflags \"${LDFLAGS:+${LDFLAGS} }-extldflags '${GO_TEST_EXT_LD_FLAGS}'\"" echo " testing: ${goipath}" checks "${goipath}" ;; provides) while read lockfile ; do dir=$(dirname "${lockfile}") goipath="${dir#${prefix}${gopath}/src/}" readmetadata "${lockfile}" provides "${goipath}" done ;; requires) while read lockfile ; do dir=$(dirname "${lockfile}") goipath="${dir#${prefix}${gopath}/src/}" readmetadata "${lockfile}" requires "${goipath}" echo "go-filesystem" done ;; esac