Gentoo Archives: gentoo-dev

From: William Hubbs <williamh@g.o>
To: gentoo-dev@l.g.o
Cc: William Hubbs <williamh@g.o>
Subject: [gentoo-dev] [PATCH 2/2] go-module.eclass: add support for EGO_SUM
Date: Wed, 26 Feb 2020 15:25:32
Message-Id: 20200226152437.15054-3-williamh@gentoo.org
In Reply to: [gentoo-dev] [PATCH 0/2] fix support for go modules by William Hubbs
1 The EGO_SUM variable replaces EGO_VENDOR for go modules.
2
3 Signed-off-by: William Hubbs <williamh@g.o>
4 ---
5 eclass/go-module.eclass | 362 +++++++++++++++++++++++++++++++++++-----
6 1 file changed, 322 insertions(+), 40 deletions(-)
7
8 diff --git a/eclass/go-module.eclass b/eclass/go-module.eclass
9 index 80ff2902b3a..68b95bbc510 100644
10 --- a/eclass/go-module.eclass
11 +++ b/eclass/go-module.eclass
12 @@ -4,28 +4,29 @@
13 # @ECLASS: go-module.eclass
14 # @MAINTAINER:
15 # William Hubbs <williamh@g.o>
16 +# @AUTHOR:
17 +# William Hubbs <williamh@g.o>
18 +# Robin H. Johnson <robbat2@g.o>
19 # @SUPPORTED_EAPIS: 7
20 # @BLURB: basic eclass for building software written as go modules
21 # @DESCRIPTION:
22 -# This eclass provides basic settings and functions
23 -# needed by all software written in the go programming language that uses
24 -# go modules.
25 -#
26 -# You will know the software you are packaging uses modules because
27 -# it will have files named go.sum and go.mod in its top-level source
28 -# directory. If it does not have these files, use the golang-* eclasses.
29 +# This eclass provides basic settings and functions needed by all software
30 +# written in the go programming language that uses modules.
31 #
32 -# If it has these files and a directory named vendor in its top-level
33 -# source directory, you only need to inherit the eclass since upstream
34 -# is vendoring the dependencies.
35 +# If the software you are packaging has a file named go.mod in its top
36 +# level directory, it uses modules and your ebuild should inherit this
37 +# eclass. If it does not, your ebuild should use the golang-* eclasses.
38 #
39 -# If it does not have a vendor directory, you should use the EGO_VENDOR
40 -# variable and the go-module_vendor_uris function as shown in the
41 -# example below to handle dependencies.
42 +# If, besides go.mod, your software has a directory named vendor in its
43 +# top level directory, the only thing you need to do is inherit the
44 +# eclass. If there is no vendor directory, you need to also populate
45 +# EGO_SUM and call go-module_set_globals as discussed below.
46 #
47 # Since Go programs are statically linked, it is important that your ebuild's
48 # LICENSE= setting includes the licenses of all statically linked
49 # dependencies. So please make sure it is accurate.
50 +# You can use a utility like dev-util/golicense (network connectivity is
51 +# required) to extract this information from the compiled binary.
52 #
53 # @EXAMPLE:
54 #
55 @@ -33,19 +34,21 @@
56 #
57 # inherit go-module
58 #
59 -# EGO_VENDOR=(
60 -# "github.com/xenolf/lego 6cac0ea7d8b28c889f709ec7fa92e92b82f490dd"
61 -# "golang.org/x/crypto 453249f01cfeb54c3d549ddb75ff152ca243f9d8 github.com/golang/crypto"
62 +# EGO_SUM=(
63 +# "github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I="
64 +# "github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo="
65 # )
66 #
67 +# go-module_set_globals
68 +#
69 # SRC_URI="https://github.com/example/${PN}/archive/v${PV}.tar.gz -> ${P}.tar.gz
70 -# $(go-module_vendor_uris)"
71 +# ${EGO_SUM_SRC_URI}"
72 #
73 # @CODE
74
75 case ${EAPI:-0} in
76 7) ;;
77 - *) die "${ECLASS} API in EAPI ${EAPI} not yet established."
78 + *) die "${ECLASS} EAPI ${EAPI} is not supported."
79 esac
80
81 if [[ -z ${_GO_MODULE} ]]; then
82 @@ -64,10 +67,12 @@ export GO111MODULE=on
83 export GOCACHE="${T}/go-build"
84
85 # The following go flags should be used for all builds.
86 -# -mod=vendor stopps downloading of dependencies from the internet.
87 # -v prints the names of packages as they are compiled
88 # -x prints commands as they are executed
89 -export GOFLAGS="-mod=vendor -v -x"
90 +# -mod=readonly do not update go.mod/go.sum but fail if updates are needed
91 +# -mod=vendor use the vendor directory instead of downloading dependencies
92 +export GOFLAGS="-v -x -mod=readonly"
93 +[[ ${#EGO_VENDOR[@]} -gt 0 ]] && GOFLAGS+=" -mod=vendor"
94
95 # Do not complain about CFLAGS etc since go projects do not use them.
96 QA_FLAGS_IGNORED='.*'
97 @@ -77,28 +82,36 @@ RESTRICT="strip"
98
99 EXPORT_FUNCTIONS src_unpack pkg_postinst
100
101 +# @ECLASS-VARIABLE: EGO_SUM
102 +# @DESCRIPTION:
103 +# This is an array based on the go.sum content from inside the target package.
104 +# Each array entry must be quoted and contain a single line from go.sum.
105 +#
106 +# The format of go.sum is described upstream here:
107 +# https://tip.golang.org/cmd/go/#hdr-Module_authentication_using_go_sum
108 +#
109 +# h1:<hash> is the Hash1 structure used by upstream Go
110 +# Note that Hash1 is MORE stable than Gentoo distfile hashing, and upstream
111 +# warns that it's conceptually possible for the Hash1 value to remain stable
112 +# while the upstream zipfiles change. E.g. it does NOT capture mtime changes in
113 +# files within a zipfile.
114 +
115 # @ECLASS-VARIABLE: EGO_VENDOR
116 # @DESCRIPTION:
117 -# This variable contains a list of vendored packages.
118 -# The items of this array are strings that contain the
119 -# import path and the git commit hash for a vendored package.
120 -# If the import path does not start with github.com, the third argument
121 -# can be used to point to a github repository.
122 +# This variable is deprecated and should no longer be used. Please
123 +# convert your ebuilds to use EGO_SUM.
124
125 # @FUNCTION: go-module_vendor_uris
126 # @DESCRIPTION:
127 -# Convert the information in EGO_VENDOR to a format suitable for
128 -# SRC_URI.
129 -# A call to this function should be added to SRC_URI in your ebuild if
130 -# the upstream package does not include vendored dependencies.
131 +# This function is deprecated.
132 go-module_vendor_uris() {
133 local hash import line repo x
134 for line in "${EGO_VENDOR[@]}"; do
135 read -r import hash repo x <<< "${line}"
136 - if [[ -n $x ]]; then
137 + if [[ -n ${x} ]]; then
138 eerror "Trailing information in EGO_VENDOR in ${P}.ebuild"
139 eerror "${line}"
140 - eerror "Trailing information is: \"$x\""
141 + eerror "Trailing information is: \"${x}\""
142 die "Invalid EGO_VENDOR format"
143 fi
144 : "${repo:=${import}}"
145 @@ -106,18 +119,224 @@ go-module_vendor_uris() {
146 done
147 }
148
149 +# @ECLASS-VARIABLE: _GOMODULE_GOPROXY_BASEURI
150 +# @DESCRIPTION:
151 +# Golang module proxy service to fetch module files from. Note that the module
152 +# proxy generally verifies modules via the Hash1 code.
153 +#
154 +# Users in China may find some mirrors in the default list blocked, and should
155 +# explicitly set an entry in /etc/portage/mirrors for goproxy to
156 +# https://goproxy.cn/ or another mirror that is not blocked in China.
157 +# See https://arslan.io/2019/08/02/why-you-should-use-a-go-module-proxy/ for
158 +# further details
159 +#
160 +# This variable is NOT intended for user-level configuration of mirrors, but
161 +# rather to cover go modules that might exist only on specific Goproxy
162 +# servers for non-technical reasons.
163 +#
164 +# This variable should NOT be present in user-level configuration e.g.
165 +# /etc/portage/make.conf, as it will violate metadata immutability!
166 +#
167 +# I am considering removing this and just hard coding mirror://goproxy
168 +# below, so please do not rely on it.
169 +: "${_GOMODULE_GOPROXY_BASEURI:=mirror://goproxy/}"
170 +
171 +# @ECLASS-VARIABLE: _GOMODULE_GOSUM_REVERSE_MAP
172 +# @DESCRIPTION:
173 +# Mapping back from Gentoo distfile name to upstream distfile path.
174 +# Associative array to avoid O(N*M) performance when populating the GOPROXY
175 +# directory structure.
176 +declare -A -g _GOMODULE_GOSUM_REVERSE_MAP
177 +
178 +# @FUNCTION: go-module_set_globals
179 +# @DESCRIPTION:
180 +# Convert the information in EGO_SUM for other usage in the ebuild.
181 +# - Populates EGO_SUM_SRC_URI that can be added to SRC_URI
182 +# - Exports _GOMODULE_GOSUM_REVERSE_MAP which provides reverse mapping from
183 +# distfile back to the relative part of SRC_URI, as needed for
184 +# GOPROXY=file:///...
185 +go-module_set_globals() {
186 + local line exts
187 + # for tracking go.sum errors
188 + local error_in_gosum=0
189 + local -a gosum_errorlines
190 + # used make SRC_URI easier to read
191 + local newline=$'\n'
192 +
193 + # Now parse EGO_SUM
194 + for line in "${EGO_SUM[@]}"; do
195 + local module version modfile version_modfile kvs x
196 + read -r module version_modfile kvs <<< "${line}"
197 +
198 + # Split 'v0.3.0/go.mod' into 'v0.3.0' and '/go.mod'
199 + # It might NOT have the trailing /go.mod
200 + IFS=/ read -r version modfile x <<<"${version_modfile}"
201 + # Reject multiple slashes
202 + if [[ -n ${x} ]]; then
203 + error_in_gosum=1
204 + gosum_errorlines+=( "Bad version: ${version_modfile}" )
205 + continue
206 + fi
207 +
208 + # The modfile variable should be either empty or '/go.mod'
209 + # There is a chance that upstream Go might add something else here in
210 + # future, and we should be prepared to capture it.
211 + # The .info files do not need to be downloaded, they will be created
212 + # based on the .mod file.
213 + # See https://github.com/golang/go/issues/35922#issuecomment-584824275
214 + exts=()
215 + local errormsg=''
216 + case "${modfile}" in
217 + '') exts=( zip ) ;;
218 + 'go.mod'|'/go.mod') exts=( mod ) ;;
219 + *) errormsg="Unknown modfile: line='${line}', modfile='${modfile}'" ;;
220 + esac
221 +
222 + # If it was a bad entry, restart the loop
223 + if [[ -n ${errormsg} ]]; then
224 + error_in_gosum=1
225 + gosum_errorlines+=( "${errormsg} line='${line}', modfile='${modfile}'" )
226 + continue
227 + fi
228 +
229 + _dir=$(_go-module_gomod_encode "${module}")
230 +
231 + for _ext in "${exts[@]}" ; do
232 + # Relative URI within a GOPROXY for a file
233 + _reluri="${_dir}/@v/${version}.${_ext}"
234 + # SRC_URI: LHS entry
235 + _uri="${_GOMODULE_GOPROXY_BASEURI}/${_reluri}"
236 +# _uri="mirror://goproxy/${_reluri}"
237 + # SRC_URI: RHS entry, encode any slash in the path as
238 + # %2F in the filename
239 + _distfile="${_reluri//\//%2F}"
240 +
241 + EGO_SUM_SRC_URI+=" ${_uri} -> ${_distfile}${newline}"
242 + _GOMODULE_GOSUM_REVERSE_MAP["${_distfile}"]="${_reluri}"
243 + done
244 + done
245 +
246 + if [[ ${error_in_gosum} != 0 ]]; then
247 + eerror "Trailing information in EGO_SUM in ${P}.ebuild"
248 + for line in "${gosum_errorlines[@]}" ; do
249 + eerror "${line}"
250 + done
251 + die "Invalid EGO_SUM format"
252 + fi
253 +
254 + # Ensure these variables are not changed past this point
255 + readonly EGO_SUM
256 + readonly EGO_SUM_SRC_URI
257 + readonly _GOMODULE_GOSUM_REVERSE_MAP
258 +
259 + # Set the guard that we are safe
260 + _GO_MODULE_SET_GLOBALS_CALLED=1
261 +}
262 +
263 # @FUNCTION: go-module_src_unpack
264 # @DESCRIPTION:
265 +# - If EGO_VENDOR is set, use the deprecated function to unpack the base
266 +# tarballs and the tarballs indicated in EGO_VENDOR to the correct
267 +# locations.
268 +# - Otherwise, if EGO_SUM is set, unpack the base tarball(s) and set up the
269 +# local go proxy.
270 +# - Otherwise do a normal unpack.
271 +go-module_src_unpack() {
272 + if [[ "${#EGO_VENDOR[@]}" -gt 0 ]]; then
273 + _go-module_src_unpack_vendor
274 + elif [[ "${#EGO_SUM[@]}" -gt 0 ]]; then
275 + _go-module_src_unpack_gosum
276 + else
277 + default
278 + fi
279 +}
280 +
281 +# @FUNCTION: _go-module_src_unpack_gosum
282 +# @DESCRIPTION:
283 +# Populate a GOPROXY directory hierarchy with distfiles from EGO_SUM and
284 +# unpack the base distfiles.
285 +#
286 +# Exports GOPROXY environment variable so that Go calls will source the
287 +# directory correctly.
288 +_go-module_src_unpack_gosum() {
289 + # shellcheck disable=SC2120
290 + debug-print-function "${FUNCNAME}" "$@"
291 +
292 + if [[ ! ${_GO_MODULE_SET_GLOBALS_CALLED} ]]; then
293 + die "go-module_set_globals must be called in global scope"
294 + fi
295 +
296 + local goproxy_dir="${T}/go-proxy"
297 + mkdir -p "${goproxy_dir}" || die
298 +
299 + # For each Golang module distfile, look up where it's supposed to go, and
300 + # symlink into place.
301 + local f
302 + local goproxy_mod_dir
303 + for f in ${A}; do
304 + goproxy_mod_path="${_GOMODULE_GOSUM_REVERSE_MAP["${f}"]}"
305 + if [[ -n "${goproxy_mod_path}" ]]; then
306 + debug-print-function "Populating go proxy for ${goproxy_mod_path}"
307 + # Build symlink hierarchy
308 + goproxy_mod_dir=$( dirname "${goproxy_dir}"/"${goproxy_mod_path}" )
309 + mkdir -p "${goproxy_mod_dir}" || die
310 + ln -sf "${DISTDIR}"/"${f}" "${goproxy_dir}/${goproxy_mod_path}" ||
311 + die "Failed to ln"
312 + local v=${goproxy_mod_path}
313 + v="${v%.mod}"
314 + v="${v%.zip}"
315 + v="${v//*\/}"
316 + _go-module_gosum_synthesize_files "${goproxy_mod_dir}" "${v}"
317 + else
318 + unpack "$f"
319 + fi
320 + done
321 + export GOPROXY="file://${goproxy_dir}"
322 +
323 + # Validate the gosum now
324 + _go-module_src_unpack_verify_gosum
325 +}
326 +
327 +# @FUNCTION: _go-module_gosum_synthesize_files
328 +# @DESCRIPTION:
329 +# Given a path & version, populate all Goproxy metadata files which aren't
330 +# needed to be downloaded directly.
331 +# - .../@v/${version}.info
332 +# - .../@v/list
333 +_go-module_gosum_synthesize_files() {
334 + local target=$1
335 + local version=$2
336 + # 'go get' doesn't care about the hash of the .info files, they
337 + # just need a 'version' element!
338 + # This saves a download of a tiny file
339 + # The .time key is omitted, because that is the time a module was added
340 + # to the upstream goproxy, and not metadata about the module itself.
341 + cat >"${target}/${version}.info" <<-EOF
342 + {
343 + "Version": "${version}",
344 + "shortName": "${version}",
345 + "Name": "${version}"
346 + }
347 + EOF
348 + listfile="${target}"/list
349 + if ! grep -sq -x -e "${version}" "${listfile}" 2>/dev/null; then
350 + echo "${version}" >>"${listfile}"
351 + fi
352 +}
353 +
354 +# @FUNCTION: _go-module_src_unpack_vendor
355 +# @DESCRIPTION:
356 # Extract all archives in ${a} which are not nentioned in ${EGO_VENDOR}
357 # to their usual locations then extract all archives mentioned in
358 # ${EGO_VENDOR} to ${S}/vendor.
359 -go-module_src_unpack() {
360 - debug-print-function ${FUNCNAME} "$@"
361 +_go-module_src_unpack_vendor() {
362 + # shellcheck disable=SC2120
363 + debug-print-function "${FUNCNAME}" "$@"
364 local f hash import line repo tarball vendor_tarballs x
365 vendor_tarballs=()
366 for line in "${EGO_VENDOR[@]}"; do
367 read -r import hash repo x <<< "${line}"
368 - if [[ -n $x ]]; then
369 + if [[ -n ${x} ]]; then
370 eerror "Trailing information in EGO_VENDOR in ${P}.ebuild"
371 eerror "${line}"
372 die "Invalid EGO_VENDOR format"
373 @@ -125,10 +344,10 @@ go-module_src_unpack() {
374 : "${repo:=${import}}"
375 vendor_tarballs+=("${repo//\//-}-${hash}.tar.gz")
376 done
377 - for f in $A; do
378 - [[ -n ${vendor_tarballs[*]} ]] && has "$f" "${vendor_tarballs[@]}" &&
379 + for f in ${A}; do
380 + [[ -n ${vendor_tarballs[*]} ]] && has "${f}" "${vendor_tarballs[@]}" &&
381 continue
382 - unpack "$f"
383 + unpack "${f}"
384 done
385
386 [[ -z ${vendor_tarballs[*]} ]] && return
387 @@ -143,6 +362,42 @@ go-module_src_unpack() {
388 -f "${DISTDIR}/${tarball}" || die
389 eend
390 done
391 + eqawarn "${P}.ebuild uses EGO_VENDOR. Please migrate to EGO_SUM."
392 +}
393 +
394 +# @FUNCTION: _go-module_src_unpack_verify_gosum
395 +# @DESCRIPTION:
396 +# Validate the Go modules declared by EGO_SUM are sufficient to cover building
397 +# the package, without actually building it yet.
398 +_go-module_src_unpack_verify_gosum() {
399 + # shellcheck disable=SC2120
400 + debug-print-function "${FUNCNAME}" "$@"
401 +
402 + if [[ ! ${_GO_MODULE_SET_GLOBALS_CALLED} ]]; then
403 + die "go-module_set_globals must be called in global scope"
404 + fi
405 +
406 + cd "${S}"
407 +
408 + # Cleanup the modules before starting anything else
409 + # This will print 'downloading' messages, but it's accessing content from
410 + # the $GOPROXY file:/// URL!
411 + einfo "Tidying go.mod/go.sum"
412 + go mod tidy >/dev/null
413 +
414 + # Verify that all needed modules are really present, by fetching everything
415 + # in the package's main go.mod. If the EGO_SUM was missing an entry then
416 + # 'go mod tidy' && 'go get' will flag it.
417 + # -v = verbose
418 + # -d = download only, don't install
419 + # -mod readonly = treat modules as readonly source
420 + einfo "Verifying linked Golang modules"
421 + go get \
422 + -v \
423 + -d \
424 + -mod readonly \
425 + all \
426 + || die "Some module is missing, update EGO_SUM"
427 }
428
429 # @FUNCTION: go-module_live_vendor
430 @@ -150,13 +405,14 @@ go-module_src_unpack() {
431 # This function is used in live ebuilds to vendor the dependencies when
432 # upstream doesn't vendor them.
433 go-module_live_vendor() {
434 - debug-print-function ${FUNCNAME} "$@"
435 + debug-print-function "${FUNCNAME}" "$@"
436
437 + # shellcheck disable=SC2086
438 has live ${PROPERTIES} ||
439 die "${FUNCNAME} only allowed in live ebuilds"
440 [[ "${EBUILD_PHASE}" == unpack ]] ||
441 die "${FUNCNAME} only allowed in src_unpack"
442 - [[ -d "${S}"/vendor ]] ||
443 + [[ -d "${S}"/vendor ]] &&
444 die "${FUNCNAME} only allowed when upstream isn't vendoring"
445
446 pushd "${S}" >& /dev/null || die
447 @@ -168,7 +424,7 @@ go-module_live_vendor() {
448 # @DESCRIPTION:
449 # Display a warning about security updates for Go programs.
450 go-module_pkg_postinst() {
451 - debug-print-function ${FUNCNAME} "$@"
452 + debug-print-function "${FUNCNAME}" "$@"
453 [[ -n ${REPLACING_VERSIONS} ]] && return 0
454 ewarn "${PN} is written in the Go programming language."
455 ewarn "Since this language is statically linked, security"
456 @@ -179,4 +435,30 @@ go-module_pkg_postinst() {
457 ewarn "stable tree."
458 }
459
460 +# @FUNCTION: _go-module_gomod_encode
461 +# @DESCRIPTION:
462 +# Encode the name(path) of a Golang module in the format expected by Goproxy.
463 +#
464 +# Upper letters are replaced by their lowercase version with a '!' prefix.
465 +#
466 +_go-module_gomod_encode() {
467 + ## Python:
468 + # return re.sub('([A-Z]{1})', r'!\1', s).lower()
469 +
470 + ## Sed:
471 + ## This uses GNU Sed extension \l to downcase the match
472 + #echo "${module}" |sed 's,[A-Z],!\l&,g'
473 + #
474 + # Bash variant:
475 + debug-print-function "${FUNCNAME}" "$@"
476 + #local re input lower
477 + re='(.*)([A-Z])(.*)'
478 + input="${1}"
479 + while [[ ${input} =~ ${re} ]]; do
480 + lower='!'"${BASH_REMATCH[2],}"
481 + input="${BASH_REMATCH[1]}${lower}${BASH_REMATCH[3]}"
482 + done
483 + echo "${input}"
484 +}
485 +
486 fi
487 --
488 2.24.1