From 6f528f5d546bab34d6dfa8fab5c5df37581de9a1 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Thu, 7 Dec 2023 14:50:26 -0500 Subject: [PATCH] Strip CLI and plugin binaries to reduce their size (#596) * Use the local "builder" version When running the tests for the sample-plugin, the "builder" plugin needs to be installed. However, it may happen that there was a change in the sample-plugin Makefile and the builder plugin code. This means we must use the local "builder" version and not the one currently published to the central repository. This commit does this by: 1. building the "builder" plugin under bin/ 2. using bin/builder to build the "builder" plugin so it can be installed under the tanzu CLI Signed-off-by: Marc Khouzam * Optimize CLI and plugin binaries to reduce their size Overall binary reduction achieved by this commit: - 28% decrease for Darwin - 36% decrease for Linux and Windows This commit does two optimizations described below. Note that these optimizations are done directly within the builder plugin and not as part of the "plugin-tooling.mk.tmpl" file. This approach will make it easier for plugin authors to get their hands on these improvements, without having to update their copy of the "plugin-tooling.mk" file (which they may not do very often). Optimizations: ============= 1. Remove symbol tables (reduce about 30% for Linux/Windows, 22% for Mac) -- When compiling the CLI and plugins, this commit adds the "-s" and "-w" flags. The "-s" flag omits the symbol table and debug information while "-w" omits the DWARF symbol table. These symbol tables are used by debuggers. The impact in removing them will be that it will not be possible to use a debugger on a production binary; this is acceptable and it is actually a standard approach to ship production binaries without debug symbols. If a debugger needs to be used, a binary can built with the symbol tables. Note that the removal of debug symbols does not prevent proper traces from being shown in a go "panic". To build a CLI with debug symbols use TANZU_CLI_ENABLE_DEBUG=1 To build plugins with debug symbols use PLUGIN_ENABLE_DEBUG=1 2. Disable function inlining (reduce about 6% on every OS) -- Adding the flag "-gcflags=all=-l" removes function inlining. This normally reduces performance a little but saves space. Considering we are dealing with a CLI tool which does not perform CPU intensive operations, the slight possible performance degradation is not of concern. Signed-off-by: Marc Khouzam --------- Signed-off-by: Marc Khouzam --- Makefile | 10 ++++++-- cmd/plugin/builder/command/cli_compile.go | 15 ++++++++++++ cmd/plugin/builder/plugin.go | 3 +++ .../plugintemplates/plugin-tooling.mk.tmpl | 16 ++++++++++++- docs/dev/README.md | 11 +++++++++ docs/plugindev/README.md | 24 +++++++++++++++++++ plugin-tooling.mk | 16 ++++++++++++- test/sample-plugin/Makefile | 4 +++- test/sample-plugin/plugin-tooling.mk | 19 +++++++++++++-- 9 files changed, 111 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 72ce0d955..2f87f0e7e 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,12 @@ LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-cli/pkg/buildinfo.Date=$(BUILD_DAT LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-cli/pkg/buildinfo.SHA=$(BUILD_SHA)' LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-cli/pkg/buildinfo.Version=$(BUILD_VERSION)' +# Remove debug symbols to reduce binary size +# To build with debug symbols: TANZU_CLI_ENABLE_DEBUG=1 +ifneq ($(strip $(TANZU_CLI_ENABLE_DEBUG)),1) +LD_FLAGS += -w -s +endif + APT_IMAGE=ubuntu ifdef APT_BUILDER_IMAGE APT_IMAGE=$(APT_BUILDER_IMAGE) @@ -133,9 +139,9 @@ build-cli-%: ##Build the Tanzu Core CLI for a platform fi @if [ "$(OS)" = "windows" ]; then \ - GOOS=$(OS) GOARCH=$(ARCH) $(GO) build --ldflags "$(LD_FLAGS)" -o "$(ARTIFACTS_DIR)/$(OS)/$(ARCH)/cli/core/$(BUILD_VERSION)/tanzu-cli-$(OS)_$(ARCH).exe" ./cmd/tanzu/main.go;\ + GOOS=$(OS) GOARCH=$(ARCH) $(GO) build -gcflags=all="-l" --ldflags "$(LD_FLAGS)" -o "$(ARTIFACTS_DIR)/$(OS)/$(ARCH)/cli/core/$(BUILD_VERSION)/tanzu-cli-$(OS)_$(ARCH).exe" ./cmd/tanzu/main.go;\ else \ - GOOS=$(OS) GOARCH=$(ARCH) $(GO) build --ldflags "$(LD_FLAGS)" -o "$(ARTIFACTS_DIR)/$(OS)/$(ARCH)/cli/core/$(BUILD_VERSION)/tanzu-cli-$(OS)_$(ARCH)" ./cmd/tanzu/main.go;\ + GOOS=$(OS) GOARCH=$(ARCH) $(GO) build -gcflags=all="-l" --ldflags "$(LD_FLAGS)" -o "$(ARTIFACTS_DIR)/$(OS)/$(ARCH)/cli/core/$(BUILD_VERSION)/tanzu-cli-$(OS)_$(ARCH)" ./cmd/tanzu/main.go;\ fi ## -------------------------------------- diff --git a/cmd/plugin/builder/command/cli_compile.go b/cmd/plugin/builder/command/cli_compile.go index 4291519f0..49da6ba7c 100644 --- a/cmd/plugin/builder/command/cli_compile.go +++ b/cmd/plugin/builder/command/cli_compile.go @@ -69,6 +69,7 @@ type PluginCompileArgs struct { PluginScopeAssociationFile string TargetArch []string GroupByOSArch bool + DebugSymbols bool } const local = "local" @@ -103,6 +104,20 @@ func setGlobals(compileArgs *PluginCompileArgs) { // Append version specific ldflag by default so that user doesn't need to pass this ldflag always. ldflags = fmt.Sprintf("%s -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.Version=%s'", ldflags, version) + + // Remove debug symbols to reduce binary size + if !compileArgs.DebugSymbols { + ldflags = fmt.Sprintf("%s -w -s", ldflags) + } + + // Disable function inlining to reduce binary size + disableInlining := "-gcflags=all=-l" + if len(goflags) > 0 { + // Append the user-defined goflags so they can override the default if needed + goflags = fmt.Sprintf("%s %s", disableInlining, goflags) + } else { + goflags = disableInlining + } } func Compile(compileArgs *PluginCompileArgs) error { diff --git a/cmd/plugin/builder/plugin.go b/cmd/plugin/builder/plugin.go index 1ddf1c6a7..cfa584ce9 100644 --- a/cmd/plugin/builder/plugin.go +++ b/cmd/plugin/builder/plugin.go @@ -39,6 +39,7 @@ type pluginBuildFlags struct { Match string PluginScopeAssociationFile string GoFlags string + DebugSymbols bool } type pluginBuildPackageFlags struct { @@ -82,6 +83,7 @@ func newPluginBuildCmd() *cobra.Command { PluginScopeAssociationFile: pbFlags.PluginScopeAssociationFile, GroupByOSArch: true, GoFlags: pbFlags.GoFlags, + DebugSymbols: pbFlags.DebugSymbols, } return command.Compile(compileArgs) @@ -96,6 +98,7 @@ func newPluginBuildCmd() *cobra.Command { pluginBuildCmd.Flags().StringVarP(&pbFlags.Match, "match", "", "*", "match a plugin name to build, supports globbing") pluginBuildCmd.Flags().StringVarP(&pbFlags.PluginScopeAssociationFile, "plugin-scope-association-file", "", "", "file specifying plugin scope association") pluginBuildCmd.Flags().StringVarP(&pbFlags.GoFlags, "goflags", "", "", "goflags to set on build") + pluginBuildCmd.Flags().BoolVarP(&pbFlags.DebugSymbols, "debug-symbols", "", false, "include debug symbols in the build") _ = pluginBuildCmd.MarkFlagRequired("version") diff --git a/cmd/plugin/builder/template/plugintemplates/plugin-tooling.mk.tmpl b/cmd/plugin/builder/template/plugintemplates/plugin-tooling.mk.tmpl index 3ce275ee5..d9d8e6d8b 100644 --- a/cmd/plugin/builder/template/plugintemplates/plugin-tooling.mk.tmpl +++ b/cmd/plugin/builder/template/plugintemplates/plugin-tooling.mk.tmpl @@ -20,8 +20,21 @@ endif PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.Date=$(PLUGIN_BUILD_DATE)' PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.SHA=$(PLUGIN_BUILD_SHA)' PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.Version=$(PLUGIN_BUILD_VERSION)' + +# This variable can be used to pass additional go flags to the build command. +# Note that the builder plugin already implicitly sets the go flags "-gcflags=all=-l" +# which disable function inlining to reduce binary size. If you want to prevent the +# use of this flag, you can set PLUGIN_GO_FLAGS to "-gcflags=all=" which will replace +# the default value. PLUGIN_GO_FLAGS ?= +# To build with debug symbols use PLUGIN_ENABLE_DEBUG=1 +ifeq ($(strip $(PLUGIN_ENABLE_DEBUG)),1) +PLUGIN_DEBUG=true +else +PLUGIN_DEBUG=false +endif + # Add supported OS-ARCHITECTURE combinations here PLUGIN_BUILD_OS_ARCH ?= linux-amd64 windows-amd64 darwin-amd64 darwin-arm64 linux-arm64 @@ -102,7 +115,8 @@ plugin-build-%: --goflags "$(PLUGIN_GO_FLAGS)" \ --os-arch $(OS)_$(ARCH) \ --match "$(PLUGIN_NAME)" \ - --plugin-scope-association-file $(PLUGIN_SCOPE_ASSOCIATION_FILE) + --plugin-scope-association-file $(PLUGIN_SCOPE_ASSOCIATION_FILE) \ + --debug-symbols=$(PLUGIN_DEBUG) .PHONY: plugin-build-packages plugin-build-packages: ## Build plugin packages diff --git a/docs/dev/README.md b/docs/dev/README.md index 6191d3064..8b27a8bf4 100644 --- a/docs/dev/README.md +++ b/docs/dev/README.md @@ -55,6 +55,17 @@ To run e2e tests for the repository: make e2e-cli-core ``` +### Debugging + +By default the CLI is built without debug symbols to reduce its binary size; +this has the side-effect of preventing the use of a debugger towards the built +binary. However, when using an IDE, a different binary is used, one built by +the IDE, and therefore the debugger can be used directly from the IDE. + +If you require using a debugger directly on a CLI binary, you have to build +a binary that includes the debug symbols. You can build such a binary by using +`TANZU_CLI_ENABLE_DEBUG=1` along with your build command. + ## Centralized Discovery of Plugins The Tanzu CLI uses a system of plugins to provide functionality to interact diff --git a/docs/plugindev/README.md b/docs/plugindev/README.md index 7cf26490e..9fbbdd866 100644 --- a/docs/plugindev/README.md +++ b/docs/plugindev/README.md @@ -231,6 +231,30 @@ well. Edit the file as appropriate. +#### Optimizations, debugging and build flags + +By default the `builder` plugin `v1.2.0` or later optimizes build flags to +reduce the binary size of the plugin being built. Two optimizations are done: +building without debug symbols (which reduces the binary size by up to 30%), +and deactivating function inlining (which reduces the binary size by about 6%). + +Building without debug symbols has the side-effect of preventing the use of a +debugger towards the built binary. However, when using an IDE, a different +binary is used, one built by the IDE, and therefore the debugger can be used +directly from the IDE. + +If you require using a debugger directly on a plugin binary, you have to build +a binary that includes the debug symbols. You can build such a binary by using +`PLUGIN_ENABLE_DEBUG=1` along with your `make` command. + +The `builder` plugin deactivates function inlining by implicitly injecting +the `-gcflags=all=-l` go flags. Be aware that if you use the `PLUGIN_GO_FLAGS` +variable to inject other `-gcflags`, you should pay attention to also include +the `all=-l` to keep the optimization. If for some reason you need to activate +function inlining (at the cost of a larger binary), you can do so by +using `PLUGIN_GO_FLAGS=-gcflags=all=` (without the `-l` specified), which will +have the effect of replacing the go flags specified by the `builder` plugin. + ### Publishing a plugin To publish one or more built plugins to a target repository, one would need to diff --git a/plugin-tooling.mk b/plugin-tooling.mk index 0ffa1908c..8ea0e06fa 100644 --- a/plugin-tooling.mk +++ b/plugin-tooling.mk @@ -24,8 +24,21 @@ endif PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.Date=$(PLUGIN_BUILD_DATE)' PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.SHA=$(PLUGIN_BUILD_SHA)' PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.Version=$(PLUGIN_BUILD_VERSION)' + +# This variable can be used to pass additional go flags to the build command. +# Note that the builder plugin already implicitly sets the go flags "-gcflags=all=-l" +# which disable function inlining to reduce binary size. If you want to prevent the +# use of this flag, you can set PLUGIN_GO_FLAGS to "-gcflags=all=" which will replace +# the default value. PLUGIN_GO_FLAGS ?= +# To build with debug symbols use PLUGIN_ENABLE_DEBUG=1 +ifeq ($(strip $(PLUGIN_ENABLE_DEBUG)),1) +PLUGIN_DEBUG=true +else +PLUGIN_DEBUG=false +endif + # Add supported OS-ARCHITECTURE combinations here PLUGIN_BUILD_OS_ARCH ?= linux-amd64 windows-amd64 darwin-amd64 darwin-arm64 linux-arm64 @@ -106,7 +119,8 @@ plugin-build-%: --goflags "$(PLUGIN_GO_FLAGS)" \ --os-arch $(OS)_$(ARCH) \ --match "$(PLUGIN_NAME)" \ - --plugin-scope-association-file $(PLUGIN_SCOPE_ASSOCIATION_FILE) + --plugin-scope-association-file $(PLUGIN_SCOPE_ASSOCIATION_FILE) \ + --debug-symbols=$(PLUGIN_DEBUG) .PHONY: plugin-build-packages plugin-build-packages: ## Build plugin packages diff --git a/test/sample-plugin/Makefile b/test/sample-plugin/Makefile index 846a901c6..f232491df 100644 --- a/test/sample-plugin/Makefile +++ b/test/sample-plugin/Makefile @@ -37,9 +37,11 @@ $(GOLANGCI_LINT): $(TOOLS_BIN_DIR) ## Install golangci-lint .PHONY: install-builder install-builder: ## Install builder + $(MAKE) -C $(ROOT_DIR_RELATIVE)/../.. prepare-builder + unset BUILDER_PLUGIN ; \ export TANZU_CLI_CEIP_OPT_IN_PROMPT_ANSWER="No" ; \ export TANZU_CLI_EULA_PROMPT_ANSWER="Yes" ; \ - tanzu plugin install builder + $(MAKE) -C $(ROOT_DIR_RELATIVE)/../.. plugin-build-install-local PLUGIN_NAME=builder .PHONEY: e2e-tests-simple-plugin ## Run all e2e tests for simple plugin e2e-tests-simple-plugin: install-builder plugin-build-local plugin-install-local e2e-tests-sample-plugin-functionality e2e-tests-sample-plugin-e2e-api diff --git a/test/sample-plugin/plugin-tooling.mk b/test/sample-plugin/plugin-tooling.mk index 61857a809..43cc4e06e 100644 --- a/test/sample-plugin/plugin-tooling.mk +++ b/test/sample-plugin/plugin-tooling.mk @@ -20,8 +20,21 @@ endif PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.Date=$(PLUGIN_BUILD_DATE)' PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.SHA=$(PLUGIN_BUILD_SHA)' PLUGIN_LD_FLAGS += -X 'github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo.Version=$(PLUGIN_BUILD_VERSION)' + +# This variable can be used to pass additional go flags to the build command. +# Note that the builder plugin already implicitly sets the go flags "-gcflags=all=-l" +# which disable function inlining to reduce binary size. If you want to prevent the +# use of this flag, you can set PLUGIN_GO_FLAGS to "-gcflags=all=" which will replace +# the default value. PLUGIN_GO_FLAGS ?= +# To build with debug symbols use PLUGIN_ENABLE_DEBUG=1 +ifeq ($(strip $(PLUGIN_ENABLE_DEBUG)),1) +PLUGIN_DEBUG=true +else +PLUGIN_DEBUG=false +endif + # Add supported OS-ARCHITECTURE combinations here PLUGIN_BUILD_OS_ARCH ?= linux-amd64 windows-amd64 darwin-amd64 darwin-arm64 linux-arm64 @@ -88,8 +101,9 @@ plugin-install-local: .PHONY: plugin-build plugin-build: $(PLUGIN_BUILD_TARGETS) generate-plugin-bundle ## Build all plugin binaries for all supported os-arch +.PHONY: plugin-build-local plugin-build-local: plugin-build-$(GOHOSTOS)-$(GOHOSTARCH) ## Build all plugin binaries for local platform - + plugin-build-%: $(eval ARCH = $(word 2,$(subst -, ,$*))) $(eval OS = $(word 1,$(subst -, ,$*))) @@ -101,7 +115,8 @@ plugin-build-%: --goflags "$(PLUGIN_GO_FLAGS)" \ --os-arch $(OS)_$(ARCH) \ --match "$(PLUGIN_NAME)" \ - --plugin-scope-association-file $(PLUGIN_SCOPE_ASSOCIATION_FILE) + --plugin-scope-association-file $(PLUGIN_SCOPE_ASSOCIATION_FILE) \ + --debug-symbols=$(PLUGIN_DEBUG) .PHONY: plugin-build-packages plugin-build-packages: ## Build plugin packages