Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: all: add GOARCHVERSION instead of "GOARM", "GOARM64", "GOAMD" & "GOAMD64" (etc..) #62129

Closed
the-hotmann opened this issue Aug 18, 2023 · 12 comments
Labels
Milestone

Comments

@the-hotmann
Copy link

the-hotmann commented Aug 18, 2023

Since I build my local golang binaries like this:

# Prepare

targets=("android/arm64" "android/arm" "darwin/amd64" "dragonfly/amd64" "freebsd/386" "freebsd/amd64" "freebsd/arm" "freebsd/arm64" "linux/386" "linux/amd64" "linux/amd64/v2" "linux/amd64/v3" "linux/arm" "linux/arm/v6" "linux/arm/v7" "linux/arm/v8" "linux/arm64" "linux/mips" "linux/mips64" "linux/mips64le" "linux/mipsle" "linux/ppc64" "linux/ppc64le" "linux/riscv64" "linux/s390x" "netbsd/386" "netbsd/amd64" "netbsd/arm" "netbsd/arm64" "openbsd/386" "openbsd/amd64" "openbsd/arm" "openbsd/arm64" "plan9/386" "plan9/amd64" "solaris/amd64" "windows/386" "windows/amd64" "windows/arm64")
go_version=$(go version | awk '{print $3}' | sed 's/go//')



# BUILD

for target in "${targets[@]}"; do
    # Split the target into OS and ARCH
    IFS="/" read -ra parts <<< "$target"
    os="${parts[0]}"
    arch="${parts[1]}"

    echo -e "\n\nOS:\t"$os
    echo -e "ARCH:\t"$arch
    echo -e "GO:\t"$go_version"\n\n"

    name="app_"$go_version"_"$os"_"$arch

    # Build the Go application
    echo "Building app for $os/$arch"
    GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -ldflags "-s -w" -o ../bin/$name -v main.go
done

I noticed that I depending on the architecture the parameter to pass to the compiler would always differ - so it would be way more easy if we would have a general parameter to pass to the compiler, something like:

GOARCH_VERSION

The we could easily loop through something like this:

# Prepare

targets=("linux/amd64" "linux/amd64/v2" "linux/amd64/v3" "linux/arm" "linux/arm/v8")
go_version=$(go version | awk '{print $3}' | sed 's/go//')


# BUILD

for target in "${targets[@]}"; do
    # Split the target into OS and ARCH
    IFS="/" read -ra parts <<< "$target"
    os="${parts[0]}"
    arch="${parts[1]}"
    vers="${parts[2]:-}"  # set to empty string if parts[2] is not present, so script does not break if GOARCH_VERSION is not provided.

    echo -e "\n\n"
    echo -e "GO:\t"$go_version
    echo -e "OS:\t"$os
    echo -e "ARCH:\t"$arch
    echo -e "VERS:\t"$vers
    echo -e "\n\n"

    name="app_"$go_version"_"$os"_"$arch$vers

    # Build the Go application
    echo "Building app for $os/$arch$vers"
    GOOS="$os" GOARCH="$arch" GOARCH_VERSION="$vers" CGO_ENABLED=0 go build -ldflags "-s -w" -o ../bin/$name -v main.go
done

I guess this makes it more easy to compile for specific architectual versions - specially in batch.
Under the hood go then will automatically know that in the case of:

"linux/amd64/v3"

GOOS="linux"
GOARCH="amd64"
GOARCH_VERSION="v3"

that GOARCH_VERSION basically is GOAMD64 and so the features of GOAMD64=v3 should be applied.

It would be awesome if we just could loop through "platforms" like in docker buildx.
Yes I know this is not docker, but it is just way more easy to compile/build for all architectures & versions of them with docker and I would like to see this at go natively :)

@dmitshur dmitshur added this to the Proposal milestone Aug 18, 2023
@ianlancetaylor ianlancetaylor changed the title all: add GOARCHVERSION instead of "GOARM", "GOARM64", "GOAMD" & "GOAMD64" (etc..) proposal: all: add GOARCHVERSION instead of "GOARM", "GOARM64", "GOAMD" & "GOAMD64" (etc..) Aug 18, 2023
@ianlancetaylor
Copy link
Contributor

It seems to me that the current approach also makes it straightforward to handle linux/amd64/v3. And the current approach permits people to set a global setting for both GO386 and GOAMD64, say, that matches the machines that they care about. So I don't see a significant benefit to adding this new feature.

@apparentlymart
Copy link

I guess there's a slight tension here between two different situations.

  1. Building one package for each overall architecture, with a single sub-architecture configuration per distinct architecture.

    For this case, as Ian says it's convenient to just set all of the per-architecture environment variables once up front and then switch GOARCH per build.

  2. Building packages for various different combinations of architecture+sub-architecture, as shown in the proposal writeup.

    In this case each build tuple must include the base architecture name, the environment variable name that represents sub-architecture of that architecture, and the value to set that sub-architecture variable to.

The current design favors 1. It does support 2, but it's annoying to implement in Bash (or similar) where there aren't flexible user-defined data types, and so representing a set of records (what might be a slice of a struct type in Go) is inconvenient.

I'm sympathetic to that additional complexity, but if we put aside the limitations of Bash and chose a language with a more advanced type system then modelling the extra environment variables doesn't seem so awkward. For example, in Go:

type BuildTarget struct {
    OS string
    Arch string
    OtherEnv map[string]string
}

# ...

targets := []BuildTarget{
    {
      OS: "android",
      Arch: "arm64",
    },
    {
      OS: "android",
      Arch: "arm64",
      OtherEnv: map[string]string{
          "GOARM64": "v2", // (I'm not sure what's actually valid for this particular environment variable)
      },
    },
    // ...
}

(Or even just a plain map of environment variables per build target, I suppose... GOOS and GOARCH don't necessarily need to be special here.)

@rittneje
Copy link

rittneje commented Aug 19, 2023

You can dynamically create the environment variable name in Bash like so:

# ...
variant="${parts[2]}"
# ...
env GOOS="${os}" GOARCH="${arch}" "GO$(tr '[:lower:]' '[:upper:]' <<< "${arch}")=${variant}" go build ...

@the-hotmann
Copy link
Author

the-hotmann commented Aug 19, 2023

I apologize for any confusion, @ianlancetaylor. It seems there might be a slight misunderstanding regarding my proposal. When I inquire Golang itself about the platforms it supports via the dist functionality, the output comprises solely the os/arch combinations, omitting any version details. As you aptly mentioned, these versions can be denoted as os/arch/vers. Here lies my predicament: how can I automatically acquire the available versions for each architecture and seamlessly pass them to Golang's builder/compiler?

My objective revolves around simplifying the process by utilizing a singular parameter for all architectures. For instance, while GOARCH64 and GOAMD64 can't coexist, the absence of conflicting versions allows for a unified approach. Considering this, wouldn't having a GOARCH_VERSION parameter encompassing all architectures and versions be an effective strategy? Such an implementation would efficiently encompass the entirety of combinations.

@apparentlymart, thank you for your precise response; it addressed my concern perfectly. As is the case with many others, I typically execute my build scripts in a shell/bash environment. Thus, I'm eager to streamline the compilation process for various architecture and version combinations. Unfortunately, achieving this isn't straightforward when considering compilation for different architectures and their respective versions.

Upon executing the command go tool dist list:

aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
freebsd/riscv64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/loong64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
wasip1/wasm
windows/386
windows/amd64
windows/arm
windows/arm64

the output displays a comprehensive list of architectures, but it notably omits any version information associated with these architectures. While this output suffices for compiling Docker images due to Docker's automated handling of subversion compilation across all available architectures, it's essential to acknowledge that Docker and Golang operate differently, as was previously understood. Even tho this output is from golang it just works as I want it to when used with docker - how bizare.

Addressing the issue raised in the discussion on GitHub (#62080), I specifically inquired about an effortless method to obtain a complete list of all feasible platforms that Golang can compile for, including architecture versions. Unfortunately, I haven't yet received a satisfactory response to my query, and the issue itself has since been closed.

Subsequently, I devised my own approach to compilation, taking into consideration your insights, @apparentlymart. However, it's worth noting that when attempting to compile for multiple architectures and versions, the distinct parameters required for each combination introduce unnecessary complexity. This complexity could potentially prove unwieldy for developers, contrary to the developer-friendly ethos we aim to uphold.

I find myself in agreement with @rittneje's suggestion, even though it might be considered a somewhat unconventional workaround. Admittedly, it does address a complexity that seems logically avoidable, given that the parameters denoting architecture versions could coexist harmoniously, even thou it is not a complete solution to the problem. Consequently, I propose a design wherein these diverse parameters (architectural versions) are consolidated under a unified parameter name. This shift could notably simplify the compilation process for multiple architectures and their respective versions, thereby enhancing the user experience and aligning with the principle of making developer tasks more accessible.

Thank you for your attention and engagement in this matter.

@ianlancetaylor
Copy link
Contributor

Building the tools for each possible GOARM, GOARM64, etc., value is an unusual operation. Even setting those environment variables is unusual. The Go runtime will normally detect the capabilities of the current CPU and do the right thing. So selecting the environment variable is an optimization tweak, that skips the capability check in some cases. I'm not convinced that we need to optimize for this uncommon case of an uncommon case.

@ianlancetaylor
Copy link
Contributor

On the other hand, if you want to be able to say go tool dist list-arch-specific amd64, or something like that, and get back a list of v1, v2, v3, v4, then that seems reasonable. That would be a separate proposal, though.

@the-hotmann
Copy link
Author

the-hotmann commented Aug 20, 2023

On the other hand, if you want to be able to say go tool dist list-arch-specific amd64, or something like that, and get back a list of v1, v2, v3, v4, then that seems reasonable. That would be a separate proposal, though.

The request you're describing aligns with the one I made here: #62080. I was seeking a solution to retrieve a comprehensive list of all feasible platforms.

However, the current proposal at hand pertains to consolidating the distinct architecture versions under a singular parameter such as GOARCH_VERSION or GPARCHVERSION.

Building the tools for each possible GOARM, GOARM64, etc., value is an unusual operation.

I hold a differing perspective on this matter for several reasons:

  1. Compiling extends beyond one's own architecture/platform due to the prevalence of cross-compilation.
  2. If such an operation is indeed uncommon, it raises the question of why Docker defaults to this practice.

Personally, I greatly value the ability to compile for all conceivable OS/ARCH/VERS combinations. This approach enables the utilization of the exact version with its inherent benefits on the most fitting hardware.

Implementing the $vers variable (representing the version of the architecture) allows multiple systems to benefit from this arrangement - all that provide it.

Thank you for engaging in this discussion; your insights are appreciated.

@ianlancetaylor
Copy link
Contributor

Personally, I greatly value the ability to compile for all conceivable OS/ARCH/VERS combinations.

I think we all agree that this can be done regardless of whether we adopt this proposal.

And I hope we all agree that we aren't going to drop the existing environment variables, as that would break existing scripts.

So the question is whether there would be enough use of GOARCHVERSION to justify the additional complexity and documentation of support another environment variable. I am arguing that setting the architecture version is fairly unusual. I'm not saying nobody does it. I'm saying that relatively few people do it. The question is whether helping those relatively few a little bit is worth the additional complexity. That could be answered by saying that lots of people want to do this, or that is really trivial to implement and document.

@rsc
Copy link
Contributor

rsc commented Nov 29, 2023

If we add GOARCHVERSION we still have to support the old GOAMD64 etc for existing build scripts. It doesn't seem worth having two at this point. Maybe if we were starting from scratch there'd be a conversation to have about GOARCHVERSION, but that ship has sailed, and it sailed to GOAMD64/GOARM64/etc island.

@rsc
Copy link
Contributor

rsc commented Dec 4, 2023

This proposal has been declined as infeasible.
— rsc for the proposal review group

@rsc rsc closed this as completed Dec 4, 2023
@the-hotmann
Copy link
Author

the-hotmann commented Dec 6, 2023

If we add GOARCHVERSION we still have to support the old GOAMD64 etc for existing build scripts.

I still disagree with this. I also remain confident that an adaptor could effectively handle both legacy GOAMD64 and the new GOARCHVERSION. It is never too late to enhance the convenience of using Golang, especially in conjunction with buildx.

To illustrate, I have prepared examples that address the specific points you raised:

I will provide these examples for ALL platform values whose architecture may include a GOARCHVERSION.

#ID Explicit Platform Implicit Platform
1 linux/amd64/v1 linux/amd64
2 linux/amd64/v2
3 linux/amd64/v3
4 linux/arm/v5
5 linux/arm/v6
6 linux/arm/v7 linux/arm
7 linux/arm64/v8 linux/arm64

For each case, I will demonstrate how the current legacy setup would look and compare it with the new one, along with the logical mapping.

RULES:

  • As of now, to the best of my knowledge, these rules only apply if GOOS is linux.
  1. if TARGETVARIANT is provided and TARGETARCH is amd64, GOAMD64 shall be set/overwrite with GOARCHVERSIONs value.
  2. if TARGETVARIANT is provided, but EMPTY and TARGETARCH is amd64, GOAMD64 shall be set/overwrite to the default value v1.
  3. if TARGETVARIANT is provided and TARGETARCH is arm, GOARM shall be set/overwrite with GOARCHVERSIONs value, but without the leading v.
  4. (note: TARGETVARIANT is not empty, when building for platform linux/arm as it defaults to v7!)
  5. if TARGETVARIANT is provided and TARGETARCH is arm64. do not implement yet, I guess it is not avialable yet. Default is v8/8

The docker buildx command looks like this for me:

docker buildx build --push \
    --platform ###PLATFORM###

Dockerfile:

[...]

# get relevat ARGs
ARG TARGETOS TARGETARCH TARGETVARIANT

# DOCKERFILE_PRINT
RUN echo $TARGETOS $TARGETARCH $TARGETVARIANT

# build with the new 'GOARCHVERSION' way
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH GOARCHVERSION=$TARGETVARIANT CGO_ENABLED=0 go build -ldflags "-s -w" -o app -v main.go

[...]
BUILDX_PLATFORM DOCKERFILE_PRINT ARCH VARIANT CLEANED_VARIANT OUTCOME_LEGACY RULE OUTCOME_NEW* WORKS?
linux/amd64 linux amd64 amd64 `` v1 GOAMD64=v1 #2 GOAMD64=v1
linux/amd64/v1 linux amd64 amd64 `` v1 GOAMD64=v1 #2 GOAMD64=v1
linux/amd64/v2 linux amd64 v2 amd64 v2 v2 GOAMD64=v2 #1 GOAMD64=v2
linux/amd64/v3 linux amd64 v3 amd64 v3 v3 GOAMD64=v3 #1 GOAMD64=v3
linux/arm linux arm v7 arm v7 7 GOARM=7 #4 GOARM=7
linux/arm/v5 linux arm v5 arm v5 5 GOARM=5 #3 GOARM=5
linux/arm/v6 linux arm v6 arm v6 6 GOARM=6 #3 GOARM=6
linux/arm/v7 linux arm v7 arm v7 7 GOARM=7 #3 GOARM=7
linux/arm64 linux arm64 arm64 `` 8 - - -
linux/arm64/v8 `` arm64 `` 8 - - -

AFAIK linux/arm64/v8 is not handlef yet, but I think it will come. So I just left the linux/arm64 section nearly empty for now. Of course it would work the very same here aswell.

*OUTCOME_NEW=THE GOAMD64, or GOARM version produced through the GOARCHVERSION adapter.

As evident, a proper mapping ensures 100% compatibility while maintaining the old way and introducing a new one. This allows developers to choose between using GOAMD64 etc. or the new GOARCHVERSION. Utilizing the new GOARCHVERSION is now fully compatible with all possible valid docker buildx --platform values, compiling directly and without any hacks, scripts, etc., directly to the desired platform.

If one does not want to use any of this, just dont use GOARCHVERSION and everything is 100% as it was. Anyway, using it, does also not change anything at the outcome.

Please consider this proposal, as it promises to simplify the lives of developers, offering enhanced convenience and backward compatibility. It also facilitates the maintenance of future platforms (for new upcomming OSs, Archs, Variants).

Best regards,
Martin

@the-hotmann
Copy link
Author

the-hotmann commented Dec 6, 2023

Here a simple golang snippet, that prints both out: The old way and the new one.

package main

import (
	"fmt"
	"strings"
)

type PlatformInfo struct {
	GOOS       string
	GOARCH     string
	GOARCHMODE string
	GOVARIANT  string
}

func main() {

	// alle validen relevanten Platform Werte
	platformValues := []string{
		"linux/amd64", "linux/amd64/v1", "linux/amd64/v2", "linux/amd64/v3",
		"linux/arm", "linux/arm/v5", "linux/arm/v6", "linux/arm/v7",
		"linux/arm64", "linux/arm64/v8",
	}

	for _, platform := range platformValues {

		parts := strings.Split(platform, "/")
		targetOS := parts[0]
		targetArch := parts[1]
		targetVariant := ""

		if len(parts) > 2 {
			targetVariant = parts[2]
		}

		// Initialize separate structs for the old and new methods
		oldInfo := PlatformInfo{
			GOOS:       targetOS,
			GOARCH:     targetArch,
			GOARCHMODE: "",
			GOVARIANT:  targetVariant,
		}

		newInfo := PlatformInfo{
			GOOS:       targetOS,
			GOARCH:     targetArch,
			GOARCHMODE: "",
			GOVARIANT:  targetVariant,
		}

		// NEUE Regeln
		if targetOS == "linux" {
			switch {
			case targetVariant != "" && targetArch == "amd64":
				newInfo.GOVARIANT = targetVariant
			case targetVariant != "" && targetArch == "arm":
				newInfo.GOVARIANT = strings.TrimPrefix(targetVariant, "v")
			case targetVariant != "" && targetArch == "arm64":
				newInfo.GOVARIANT = strings.TrimPrefix(targetVariant, "v")
			case targetVariant == "" && targetArch == "amd64":
				newInfo.GOVARIANT = "v1"
				newInfo.GOARCHMODE = "" // Clear GOARCHMODE for the new method
			case targetVariant == "" && targetArch == "arm64":
				newInfo.GOVARIANT = "8"
			case targetVariant == "" && targetArch == "arm":
				newInfo.GOVARIANT = "7"
			}
		}

		// ALTE Regeln
		switch targetArch {
		case "amd64":
			oldInfo.GOARCHMODE = "GOAMD64"
		case "arm":
			oldInfo.GOARCHMODE = "GOARM"
			oldInfo.GOVARIANT = strings.TrimPrefix(targetVariant, "v")
			if oldInfo.GOVARIANT == "" {
				oldInfo.GOVARIANT = "7"
			}
		case "arm64":
			oldInfo.GOARCHMODE = "GOARM64"
			oldInfo.GOVARIANT = strings.TrimPrefix(targetVariant, "v")
		}

		// Print the mapped values for the new and old ways
		fmt.Printf("Platform:\t%s\nOLD values:\tGOOS=%s GOARCH=%s", platform, oldInfo.GOOS, oldInfo.GOARCH)
		if oldInfo.GOARCHMODE != "" && oldInfo.GOVARIANT != "" {
			fmt.Printf(" %s=%s", oldInfo.GOARCHMODE, oldInfo.GOVARIANT)
		}
		fmt.Printf("\nNEW values:\tGOOS=%s GOARCH=%s", newInfo.GOOS, newInfo.GOARCH)
		if newInfo.GOVARIANT != "" {
			fmt.Printf(" GOARCHVERSION=%s", newInfo.GOVARIANT)
		}
		fmt.Println("\n")
	}
}

This is just a PoW of the Mapping and the rules.
Notice how the new way is mor explicit. If provided with linux/amd64 it maps it to linux/amd64/v1 as v1 is the default for linux/amd64.
This ofc can be changed, if not desired.

If you execute the programm it maps them like this:

Output:

Platform:       linux/amd64
OLD values:     GOOS=linux GOARCH=amd64
NEW values:     GOOS=linux GOARCH=amd64 GOARCHVERSION=v1

Platform:       linux/amd64/v1
OLD values:     GOOS=linux GOARCH=amd64 GOAMD64=v1
NEW values:     GOOS=linux GOARCH=amd64 GOARCHVERSION=v1

Platform:       linux/amd64/v2
OLD values:     GOOS=linux GOARCH=amd64 GOAMD64=v2
NEW values:     GOOS=linux GOARCH=amd64 GOARCHVERSION=v2

Platform:       linux/amd64/v3
OLD values:     GOOS=linux GOARCH=amd64 GOAMD64=v3
NEW values:     GOOS=linux GOARCH=amd64 GOARCHVERSION=v3

Platform:       linux/arm
OLD values:     GOOS=linux GOARCH=arm GOARM=7
NEW values:     GOOS=linux GOARCH=arm GOARCHVERSION=7

Platform:       linux/arm/v5
OLD values:     GOOS=linux GOARCH=arm GOARM=5
NEW values:     GOOS=linux GOARCH=arm GOARCHVERSION=5

Platform:       linux/arm/v6
OLD values:     GOOS=linux GOARCH=arm GOARM=6
NEW values:     GOOS=linux GOARCH=arm GOARCHVERSION=6

Platform:       linux/arm/v7
OLD values:     GOOS=linux GOARCH=arm GOARM=7
NEW values:     GOOS=linux GOARCH=arm GOARCHVERSION=7

Platform:       linux/arm64
OLD values:     GOOS=linux GOARCH=arm64
NEW values:     GOOS=linux GOARCH=arm64 GOARCHVERSION=8

Platform:       linux/arm64/v8
OLD values:     GOOS=linux GOARCH=arm64 GOARM64=8
NEW values:     GOOS=linux GOARCH=arm64 GOARCHVERSION=8

So GOARCHVERSION actually can replace ALL currently available other variant-options all at once.
This proves, that following the rules, you can easily replace all of them with GOARCHVERSION and map it then onto them. The users then just have to pass platforms in and just map them like this:

GOOS=$TARGETOS GOARCH=$TARGETARCH GOARCHVERSION=$TARGETVARIANT go build [...]

(after the mapping is done)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Declined
Development

No branches or pull requests

6 participants