# Copyright 2022 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # @ECLASS: go.eclass # @MAINTAINER: # Ryan Qian # @AUTHOR: # Ryan Qian # @SUPPORTED_EAPIS: 7 8 # @BLURB: basic eclass for building software written in golang # @DESCRIPTION: # This eclass provides basic settings and functions needed by software # written in the go programming language. # # This eclass has three methods for offline building and one methods for online building: # # THREE OFFLINE BUILDING METHODS: # # ** priority: 1. > 2. > 3. ** # # 1. for packages with the go.sum file which line number is less than GO_SUM_LIST_MAX (default to 100), # if the corresponding go.sum file exists in the 'files' directory with name 'go.sum.$PV', this ebuild # will automatically set the local proxy url for all modules in the 'go.sum' file. You just need to # add the GO_SUM_LIST_SRC_URI variable into the SRC_URI variable. # # @CODE # # inherit go # # SRC_URI="https://github.com/example-org/reponame/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz" # SRC_URI+=" ${GO_SUM_LIST_SRC_URI}" # # @CODE # # 2. use project embedded 'vendor' directory to build. # just inherit this eclass, don't need to do other special works # # @CODE # # inherit go # # @CODE # # 3. use extra 'vendor' directory to offer offline building, the vendor tarball can # be generated by the script https://github.com/bekcpear/vendor-for-go . # The tarball should consist of the directory architecture: # (name quoted by '[]' means optional) # # [any-parent-directory/] # # vendor/ # # [go-mod-sum.diff] # should and expected to be empty if # # the upstream did `go mod tidy` before releasing # # @CODE # # inherit go # # SRC_URI="https://github.com/example-org/reponame/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz # https://some.url/corresponding-vendor-path.tar.gz -> ${P}-vendor.tar.gz" # # @CODE # # THE ONLINE BUILDING METHOD: # # 1. If there is no corresponding 'go.sum.$PV' file in the 'files' directory and # also no embedded 'vendor' directory under the $S path. # This eclass will check whether the 'live' PROPERTY exists, if it is, this eclass # will use `go mod vendor` to generate the 'vendor' directory through the network # instead of checking the vendor tarball. # # @CODE # # inherit git-r3 go # # EGIT_REPO_URI="https://github.com/example-org/reponame.git" # # src_unpack() { # git-r3_src_unpack # go_set_go_cmd # go_setup_vendor # } # # OR without using the git-r3 eclass and specifying the PROPERTIES manually: # #PROPERTIES="live" # #SRC_URI="https://github.com/example-org/reponame/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz" # #src_unpack() { # # go_src_unpack # OR just omit this whole "src_unpack" function # #} # # @CODE # # # ############################################################# # ############################################################# # # Why use this eclass instead of the offical go-module.eclass, please refer to # https://github.com/bekcpear/ryans-repos/issues/4 # # Since Go programs are statically linked, it is important that your ebuild's # LICENSE= setting includes the licenses of all modules. # This script will be helpful to get the job done: # https://github.com/bekcpear/go-licenses-for-gentoo # if [[ -z ${_ECLASS_GO} ]]; then _ECLASS_GO=1 case ${EAPI} in 7|8) ;; *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; esac inherit edo version BDEPEND=">=dev-lang/go-1.16" EXPORT_FUNCTIONS src_unpack src_compile src_install IUSE="pie" # @ECLASS_VARIABLE: GOFLAGS # @DESCRIPTION: # the default GOFLAGS. # -trimpath remove all file system paths from the resulting executable # -v prints the names of packages as they are compiled # -work prints the temporary work dir's name and don't delete it when exiting # -x prints the commands export GOFLAGS="-trimpath -v -work -x" # Respect the job number explictly set in the MAKEOPTS variable if exists. # No number means the max number of available cpus which should be the same # as the default GOMAXPROCS (defaults to nproc), so should be ignored. # Should use GOMAXPROCS env var instead of the '-p' flag here due to: # 1. '-p' only limits the number of parallel building goroutines # 2. '-p' leaves the parallel number of GC, compiling and so on unchanged # 3. when '-p' is set to 1, the default '-c' for the compile tool will be set to # the GOMAXPROCS instead of min(4, GOMAXPROCS) # 4. GOMAXPROCS env variable can affect through the whole process, # N means N building goroutines and each building goroutine will call the compile # tool with '-c=I' which I=min(4, N). This is the behavior that best matches -j. # (The max number of -c may change in the future) # Also see https://go-review.googlesource.com/c/go/+/41192 for the performance of # concurrency of compiling process. [[ $MAKEOPTS =~ (-[a-z]*j|--jobs[=[:space:]])[[:space:]]*([0-9]+) ]] || true _MAXPROCS=${BASH_REMATCH[2]} if [[ -n $_MAXPROCS ]]; then export GOMAXPROCS=$_MAXPROCS fi unset _MAXPROCS # @ECLASS_VARIABLE: EXTRA_GOFLAGS # @DESCRIPTION: # the extra GOFLAGS environment variable, default is empty, # this value will be appended to the GOFLAGS if provided. # Only valid when using the default src_compile of this eclass # or 'go_build' function of this eclass. # @ECLASS_VARIABLE: QA_FLAGS_IGNORED # @INTERNAL # @DESCRIPTION: # ignore FLAGS due to go projects do not use them, # this is a regex used by sed (without leading ^ and ending $). QA_FLAGS_IGNORED='.*' # Go packages should not be stripped with strip(1). RESTRICT+=" strip" # @ECLASS_VARIABLE: GO_LDFLAGS # @DESCRIPTION: # Flags pass to -ldflags, '-s' and '-w' flags will always be # applied except calling go command directly. # @ECLASS_VARIABLE: GO_SBIN # @DESCRIPTION: # names of binaries which should be installed as sbin # @ECLASS_VARIABLE: GO_TAGS # @DESCRIPTION: # a comma-separated list of additional build tags to consider satisfied # during the build # # @ECLASS_VARIABLE: GO_TARGET_PKGS # @DESCRIPTION: # A space-separated list of patterns which describe paths and target names # of packages which should be built instead of the default '.' or './cmd/...' . # The pattern has the following form: # [ -> ] # is relative to the current temporary build dir (normally $S) # is the binary name instead of the package path name # e.g.: # ./main -> foo # ./cmd/bar # @ECLASS_VARIABLE: GO_LDFLAGS_EXMAP # @DESCRIPTION: # Extra "variable name <-> output command" maps, the output command will be called # and assign the standard output to the corresponding variable in src_compile phase. # These variables will replace the corresponding formatted strings in GO_LDFLAGS. # The formatted string should be like '@@VARIABLE-NAME@@' # e.g.: # GO_LDFLAGS_EXMAP[BUILD_DATE]="date '+%F %T%z'" # GO_LDFLAGS="-X 'main.buildDate=@@BUILD_DATE@@'" declare -A -g GO_LDFLAGS_EXMAP # @ECLASS_VARIABLE: _GO_LDFLAGS_EXMAP_CACHE # @INTERNAL # @DESCRIPTION: # cache for GO_LDFLAGS_EXMAP declare -A -g _GO_LDFLAGS_EXMAP_CACHE # @ECLASS_VARIABLE: GO_CMD # @DESCRIPTION: # The version matched Go command used to build packages. # Default is 'go'. # When dev-lang/go is installed from the 'ryans' repo, the slot is enabled, # and the default is the latest version, may be restricted to the penultimate # latest version due to the requirement of BDEPEND. GO_CMD=go # @ECLASS_VARIABLE: GO_SUM_LIST_MAX # @DESCRIPTION: # The max line number of go.sum which can be used to set a local proxy, # default to 100. This variable should be set before the eclass inherited. : "${GO_SUM_LIST_MAX:=100}" # @ECLASS_VARIABLE: GO_SUM_LIST_SRC_URI # @DESCRIPTION: # SRC_URI for go.sum entiries GO_SUM_LIST_SRC_URI= # @ECLASS_VARIABLE: GO_SUM_LIST_SRC_URI_R # @INTERNAL # @DESCRIPTION: # (internal variable) reversed map for GO_SUM_LIST_SRC_URI declare -A -g GO_SUM_LIST_SRC_URI_R # @FUNCTION: _go_escape_go_sum_path # @INTERNAL # @DESCRIPTION: # convert all capital letters in path to '!' format _go_escape_go_sum_path() { local path="${1}" l while [[ "${path}" =~ (.*)([[:upper:]])(.*) ]]; do l=${BASH_REMATCH[2]@L} path="${BASH_REMATCH[1]}!${l}${BASH_REMATCH[3]}" done echo -n "${path}" } # @FUNCTION: _go_set_go_sum_list_src_uri # @INTERNAL # @DESCRIPTION: # set GO_SUM_LIST_SRC_URI _go_set_go_sum_list_src_uri() { debug-print-function "${FUNCNAME}" "${@}" if [[ -n ${GO_SUM_LIST_SRC_URI} ]]; then return 0 fi local _go_sum_list_file="${EBUILD%/*}/files/go.sum.$PV" if [[ ! -f "${_go_sum_list_file}" ]]; then return 0 fi local -a _go_sum_list while read -r line; do _go_sum_list+=("${line}") done <"${_go_sum_list_file}" if [[ ${#_go_sum_list[@]} -gt ${GO_SUM_LIST_MAX} ]]; then return 0 fi local _distfile_name _src_uri _ver _ext for line in "${_go_sum_list[@]}"; do <<<$(_go_escape_go_sum_path "${line}") read -r path ver _ _ext=".zip" _ver=${ver%/go.mod} if [[ ${_ver} != ${ver} ]]; then _ext=".mod" fi path="${path}/@v/${_ver}${_ext}" _distfile_name="${path//\//%2F}" eval "GO_SUM_LIST_SRC_URI_R['${_distfile_name}']='${path}'" GO_SUM_LIST_SRC_URI+=" mirror://goproxy/${path} -> ${_distfile_name}" done export GOPROXY="file://${T}/go-proxy" } _go_set_go_sum_list_src_uri # @FUNCTION: go_version # @USAGE: [-f] # @DESCRIPTION: # Get version with format major.minor of the current executing go binary, # optionally specify -f to show the full version with the patch number. go_version() { debug-print-function "${FUNCNAME}" "${@}" local output=$($GO_CMD version | cut -d' ' -f3) \ major= minor= patch= IFS='.' read major minor patch <<<"$output" major=${major#go} if [[ $1 == '-f' ]] && [[ -n $patch ]]; then output="${major}.${minor}.${patch}" else output="${major}.${minor}" fi echo -n $output } # @FUNCTION: go_set_go_cmd # @DESCRIPTION: # Try to get the version matched Go command, this is useful when the # go module to compile is restricted to the penultimate latest go version, # only functional when used with multi-version support of Go from # the ryans repository or other similar. # This function is called within the src_unpack phase by default. go_set_go_cmd() { debug-print-function "${FUNCNAME}" "${@}" local goCmdPath= local -a versions=() goCmds=() for goCmdPath in $(ls -1v "${EROOT}"/usr/bin/go[[:digit:]].[[:digit:]]* 2>/dev/null); do [[ -x "$goCmdPath" ]] || continue goCmds+=( ${goCmdPath} ) versions+=( ${goCmdPath##*go} ) done if [[ ${#versions[@]} -eq 0 ]]; then # just return it if has no multi-version go binaries return 0 fi local _bdeps bdeps i d _ver_range _opt _ver _slot use usemark local -a contained local -i depth=0 local ver_range slot _bdeps=( $(echo "$BDEPEND" | sed -E 's@(^|[[:space:]])[>=<~]{0,2}[[:alnum:]_\.-]+/[^g][^o][^[:space:]]*@ @g') ) contained[0]=1 for d in ${_bdeps[@]}; do if [[ $d =~ ^(!?)([[:alpha:]][[:alnum:]]+)\?$ ]]; then usemark=${BASH_REMATCH[1]} use=${BASH_REMATCH[2]} (( depth+=1 )) if eval "$usemark use $use"; then eval "contained[$depth]=1" else eval "contained[$depth]=0" fi elif [[ $d == "(" ]]; then : elif [[ $d == ")" ]]; then (( depth-=1 )) elif [[ $d == "||" ]]; then # ignore it due to dev-lang/go dependency should not inside this (( depth+=1 )) eval "contained[$depth]=0" else if [[ ${contained[$depth]} == 1 ]]; then bdeps+=( "$d" ) fi fi done for d in ${bdeps[@]}; do if [[ $d =~ ([>=<~]*)dev-lang/go(-([^[:space:]:]+))?(:([^[:space:]=]*))? ]]; then _opt=${BASH_REMATCH[1]} _ver=${BASH_REMATCH[3]} _slot="${BASH_REMATCH[5]}" if [[ $_opt =~ ^[=~] && -n $_ver ]]; then ver_range="= $_ver" # break loop when caught an exact version break elif [[ $_opt =~ [\>\<] && -n $_ver ]]; then _ver_range="$_opt $_ver" elif [[ -z $_slot ]]; then _ver_range= elif [[ -n $_slot ]]; then slot=$_slot fi if [[ -n $_ver_range ]]; then ver_range=$(version_make_range "$ver_range" "$_ver_range") fi fi done local ver oS oE vS vE # match non-zero slot first then ver_range for (( i = 0; i < ${#versions[@]}; i++ )); do ver=${versions[$i]} if [[ -n $slot && $slot != 0 ]]; then if ! version_compare e $ver $slot; then continue fi elif [[ -n $ver_range ]]; then read -r oS vS oE vE <<<"$ver_range" if [[ $oS == "=" ]]; then VERSION_COMPARED_PARTS="minor" if ! version_compare e $ver $vS; then continue fi else if [[ ${oS:1:1} == "=" ]]; then if version_compare l $ver $vS; then continue fi else if version_compare le $ver $vS; then continue fi fi if [[ -n $oE ]]; then if [[ ${oE:1:1} == "=" ]]; then if version_compare g $ver $vE; then continue fi else if version_compare ge $ver $vE; then continue fi fi fi fi fi GO_CMD="${goCmds[$i]}" done } # @FUNCTION: go_set_pie # @DESCRIPTION: # Setup the buildmode to pie when pie USE enabled and supported. go_set_pie() { debug-print-function "${FUNCNAME}" "${@}" if ! use pie; then return 0 fi local pie_supported=0 supported_platform="" target_platform="" target_platform="$($GO_CMD env GOOS)/$($GO_CMD env GOARCH)" local supported_platforms=( "linux/386" "linux/amd64" "linux/arm" "linux/arm64" "linux/ppc64le" "linux/riscv64" "linux/s390x" "android/amd64" "android/arm" "android/arm64" "android/386" "freebsd/amd64" "darwin/amd64" "darwin/arm64" "ios/amd64" "ios/arm64" "aix/ppc64" "windows/386" "windows/amd64" "windows/arm" "windows/arm64" ) for supported_platform in "${supported_platforms[@]}"; do if [[ $supported_platform == "$target_platform" ]]; then pie_supported=1 break fi done if [[ $pie_supported == 1 ]]; then GOFLAGS="-buildmode=pie ${GOFLAGS}" else eerror "PIE: unsupported platform '${target_platform}', ignore the 'pie' USE flag!" fi } # @FUNCTION: go_setup_proxy # @DESCRIPTION: # Setup the local proxy for downloading go modules. go_setup_proxy() { debug-print-function "${FUNCNAME}" "${@}" local -a _default_A local _f _go_proxy_dir="${GOPROXY#file:\/\/}" mkdir -p ${_go_proxy_dir} || die for f in ${A}; do _f="${GO_SUM_LIST_SRC_URI_R[${f}]}" if [[ -n ${_f} ]]; then _f="${_go_proxy_dir}/${_f}" mkdir -p "$(dirname ${_f})" || die ln -sf ${DISTDIR}/${f} ${_f} || die else _default_A+=("${f}") fi done if [[ "$1" == "i" ]]; then declare -p _default_A fi } # @FUNCTION: go_setup_vendor # @DESCRIPTION: # setup vendor directory go_setup_vendor() { debug-print-function "${FUNCNAME}" "${@}" if [[ ! -d "${S}/vendor" ]]; then if [[ ${PROPERTIES} =~ (^|[[:space:]])live([[:space:]]|$) ]]; then # Golang does not support the 'socks5h://' schema for http[s]_proxy env variable: # https://github.com/golang/go/blob/9123221ccf3c80c741ead5b6f2e960573b1676b9/src/vendor/golang.org/x/net/http/httpproxy/proxy.go#L152-L159 # while libcurl supports it: # https://github.com/curl/curl/blob/ae98b85020094fb04eee7e7b4ec4eb1a38a98b98/docs/libcurl/opts/CURLOPT_PROXY.3#L48-L59 # So, if a 'https_proxy=socks5h://127.0.0.1:1080' env has been set in the # make.conf to make curl (assuming curl is the current download command) to # download all packages through the proxy, go-module_live_vendor will # fail. # The only difference between these two schemas is, 'socks5h' will solve # the hostname via the proxy while 'socks5' will not. I think it's ok to # fallback 'socks5h' to 'socks5' for `go vendor` command and warn user, # until golang supports it. # related to issue: https://github.com/golang/go/issues/24135 local hp local -a hps if [[ -n $HTTP_PROXY ]]; then hps+=( HTTP_PROXY ) elif [[ -n $http_proxy ]]; then hps+=( http_proxy ) fi if [[ -n $HTTPS_PROXY ]]; then hps+=( HTTPS_PROXY ) elif [[ -n $https_proxy ]]; then hps+=( https_proxy ) fi for hp in "${hps[@]}"; do if [[ -n ${!hp} ]] && [[ ${!hp} =~ ^socks5h:// ]]; then set -- export ${hp}="socks5${!hp#socks5h}" ewarn "golang does not support the 'socks5h://' schema for '${hp}', fallback to the 'socks5://' schema" einfo "${@}" "${@}" fi done pushd "${S}" >/dev/null || die # We don't care the compatibility with other go versions due to it's a temporary dir and the # only purpose here is to build this package under current version of the go binary, # so specify a compatible go version with current version number here to avoid incompatibility, # such as go1.16 and go1.17 has different build list calculation methods (https://go.dev/ref/mod#graph-pruning). edo $GO_CMD mod tidy -compat $(go_version) edo $GO_CMD mod vendor popd >/dev/null || die else local -a vendors local vendor go_mod_sum_diff vendors=($(find "${WORKDIR}" -maxdepth 2 -name 'vendor' 2>/dev/null || true)) if [[ ${#vendors[@]} -gt 0 ]]; then vendor="${vendors[0]}" mv "${vendor}" "${S}" || die go_mod_sum_diff="$(dirname ${vendor})/go-mod-sum.diff" if [[ -s "${go_mod_sum_diff}" ]]; then pushd "${S}" >/dev/null || die eapply "${go_mod_sum_diff}" popd >/dev/null || die fi fi fi fi } # @FUNCTION: go_src_unpack # @DESCRIPTION: # src_unpack go_src_unpack() { debug-print-function "${FUNCNAME}" "${@}" go_set_go_cmd go_set_pie if [[ -n ${GO_SUM_LIST_SRC_URI} ]]; then # prepare local proxy eval "$(go_setup_proxy i)" || die for f in "${_default_A[@]}"; do unpack "${f}" done else # prepare vendor directory default go_setup_vendor fi } # @FUNCTION: _go_print_cmd # @INTERNAL # @USAGE: ... # @DESCRIPTION: # print the command and arguments with a pretty format _go_print_cmd() { local msg is_cmd echo -ne "\x1b[32;01m===\x1b[0m" for msg; do if [[ ${msg} =~ [[:space:]] ]] && [[ -n ${is_cmd} ]]; then msg="\"${msg//\"/\\\"}\"" elif [[ ${msg} == $GO_CMD ]]; then is_cmd=1 fi echo -ne " ${msg}" done echo } # @FUNCTION: go_build # @USAGE: [-o ] ... # @DESCRIPTION: # parse necessary arguments for go build and build packages, # the binaries will be installed into the ${T}/go-bin/ directory by default. go_build() { debug-print-function "${FUNCNAME}" "${@}" local go_ldflags="${GO_LDFLAGS}" [[ "${go_ldflags}" =~ (^|[[:space:]])-w([[:space:]]|$) ]] || go_ldflags="-w ${go_ldflags}" [[ "${go_ldflags}" =~ (^|[[:space:]])-s([[:space:]]|$) ]] || go_ldflags="-s ${go_ldflags}" local key value for key in "${!GO_LDFLAGS_EXMAP[@]}"; do if [[ -n "${_GO_LDFLAGS_EXMAP_CACHE[$key]}" ]]; then value="${_GO_LDFLAGS_EXMAP_CACHE[$key]}" else value=$(eval "${GO_LDFLAGS_EXMAP[$key]}" || true) if [[ -z ${value} ]]; then die "the stdout of command '$GO_LDFLAGS_EXMAP[$key]' (GO_LDFLAGS_EXMAP[$key]) is empty" fi _GO_LDFLAGS_EXMAP_CACHE[$key]=${value} fi go_ldflags=$(<<<"${go_ldflags}" sed "s/@@${key}@@/${value}/g") done local output="${T}/go-bin/" local -a args while :; do case "${1}" in -o) shift output="${1}" shift ;; "") break ;; *) args+=( "${1}" ) shift ;; esac done set -- "${args[@]}" GOFLAGS="${GOFLAGS}${EXTRA_GOFLAGS:+ }${EXTRA_GOFLAGS}" set -- $GO_CMD build -o "${output}" ${GO_TAGS:+-tags} ${GO_TAGS} -ldflags "${go_ldflags}" "${@}" $GO_CMD env _go_print_cmd " GOFLAGS:" "${GOFLAGS}" _go_print_cmd "Build command:" "${@}" "${@}" || die } # @FUNCTION: go_src_compile # @DESCRIPTION: # src_compile go_src_compile() { debug-print-function "${FUNCNAME}" "${@}" if [[ -d "cmd" ]] && [[ -z ${GO_TARGET_PKGS} ]] && \ [[ $(find cmd/ -maxdepth 2 -type f -name '*.go' -exec \ grep -E '^package[[:space:]]+main([[:space:]]|$)' '{}' \; 2>/dev/null || true) != "" ]]; then go_build ./cmd/... elif [[ -z ${GO_TARGET_PKGS} ]]; then go_build . else local pkg_path local -a pkg_paths set -- ${GO_TARGET_PKGS} while :; do case "${1}" in '') break ;; '->') shift go_build -o "${T}/go-bin/${1}" ${pkg_path} pkg_path= shift ;; *) if [[ ${pkg_path} != "" ]]; then pkg_paths+=( "${pkg_path}" ) pkg_path= fi pkg_path="${1}" shift ;; esac done if [[ ${pkg_path} != "" ]]; then pkg_paths+=( "${pkg_path}" ) fi if [[ ${#pkg_paths[@]} -gt 0 ]]; then go_build "${pkg_paths[@]}" fi fi } # @FUNCTION: go_src_install # @DESCRIPTION: # src_install go_src_install() { debug-print-function "${FUNCNAME}" "${@}" pushd "${T}"/go-bin >/dev/null || die local _sb _sbin if [[ $(declare -p GO_SBIN 2>/dev/null) =~ declare[[:space:]]+-a ]]; then _sbin="${GO_SBIN[*]}" else _sbin="${GO_SBIN}" fi for _sb in ${_sbin}; do if ls ${_sb} &>/dev/null; then dosbin ${_sb} rm -f ${_sb} || die fi done dobin * popd >/dev/null || die } fi