Skip to content

Commit 611e16c

Browse files
Allow linker to perform deadcode elimination for program using Cobra (#1956)
* Restructure code to let linker perform deadcode elimination step Cobra, in its default configuration, will execute a template to generate help, usage and version outputs. Text/template execution calls MethodByName and MethodByName disables dead code elimination in the Go linker, therefore all programs that make use of cobra will be linked with dead code elimination disabled, even if they end up replacing the default usage, help and version formatters with a custom function and no actual text/template evaluations are ever made at runtime. Dead code elimination in the linker helps reduce disk space and memory utilization of programs. For example, for the simple example program used by TestDeadcodeElimination 40% of the final executable size is dead code. For a more realistic example, 12% of the size of Delve's executable is deadcode. This PR changes Cobra so that, in its default configuration, it does not automatically inhibit deadcode elimination by: 1. changing Cobra's default behavior to emit output for usage and help using simple Go functions instead of template execution 2. quarantining all calls to template execution into SetUsageTemplate, SetHelpTemplate and SetVersionTemplate so that the linker can statically determine if they are reachable Co-authored-by: Marc Khouzam <[email protected]>
1 parent 09d5664 commit 611e16c

File tree

6 files changed

+365
-61
lines changed

6 files changed

+365
-61
lines changed

Diff for: cobra.go

+10-6
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,16 @@ func rpad(s string, padding int) string {
176176
return fmt.Sprintf(formattedString, s)
177177
}
178178

179-
// tmpl executes the given template text on data, writing the result to w.
180-
func tmpl(w io.Writer, text string, data interface{}) error {
181-
t := template.New("top")
182-
t.Funcs(templateFuncs)
183-
template.Must(t.Parse(text))
184-
return t.Execute(w, data)
179+
func tmpl(text string) *tmplFunc {
180+
return &tmplFunc{
181+
tmpl: text,
182+
fn: func(w io.Writer, data interface{}) error {
183+
t := template.New("top")
184+
t.Funcs(templateFuncs)
185+
template.Must(t.Parse(text))
186+
return t.Execute(w, data)
187+
},
188+
}
185189
}
186190

187191
// ld compares two strings and returns the levenshtein distance between them.

Diff for: cobra_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
package cobra
1616

1717
import (
18+
"os"
19+
"os/exec"
20+
"path/filepath"
21+
"runtime"
22+
"strings"
1823
"testing"
1924
"text/template"
2025
)
@@ -222,3 +227,75 @@ func TestRpad(t *testing.T) {
222227
})
223228
}
224229
}
230+
231+
// TestDeadcodeElimination checks that a simple program using cobra in its
232+
// default configuration is linked taking full advantage of the linker's
233+
// deadcode elimination step.
234+
//
235+
// If reflect.Value.MethodByName/reflect.Value.Method are reachable the
236+
// linker will not always be able to prove that exported methods are
237+
// unreachable, making deadcode elimination less effective. Using
238+
// text/template and html/template makes reflect.Value.MethodByName
239+
// reachable.
240+
// Since cobra can use text/template templates this test checks that in its
241+
// default configuration that code path can be proven to be unreachable by
242+
// the linker.
243+
//
244+
// See also: https://github.com/spf13/cobra/pull/1956
245+
func TestDeadcodeElimination(t *testing.T) {
246+
if runtime.GOOS == "windows" {
247+
t.Skip("go tool nm fails on windows")
248+
}
249+
250+
// check that a simple program using cobra in its default configuration is
251+
// linked with deadcode elimination enabled.
252+
const (
253+
dirname = "test_deadcode"
254+
progname = "test_deadcode_elimination"
255+
)
256+
_ = os.Mkdir(dirname, 0770)
257+
defer os.RemoveAll(dirname)
258+
filename := filepath.Join(dirname, progname+".go")
259+
err := os.WriteFile(filename, []byte(`package main
260+
261+
import (
262+
"fmt"
263+
"os"
264+
265+
"github.com/spf13/cobra"
266+
)
267+
268+
var rootCmd = &cobra.Command{
269+
Version: "1.0",
270+
Use: "example_program",
271+
Short: "example_program - test fixture to check that deadcode elimination is allowed",
272+
Run: func(cmd *cobra.Command, args []string) {
273+
fmt.Println("hello world")
274+
},
275+
Aliases: []string{"alias1", "alias2"},
276+
Example: "stringer --help",
277+
}
278+
279+
func main() {
280+
if err := rootCmd.Execute(); err != nil {
281+
fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err)
282+
os.Exit(1)
283+
}
284+
}
285+
`), 0600)
286+
if err != nil {
287+
t.Fatalf("could not write test program: %v", err)
288+
}
289+
buf, err := exec.Command("go", "build", filename).CombinedOutput()
290+
if err != nil {
291+
t.Fatalf("could not compile test program: %s", string(buf))
292+
}
293+
defer os.Remove(progname)
294+
buf, err = exec.Command("go", "tool", "nm", progname).CombinedOutput()
295+
if err != nil {
296+
t.Fatalf("could not run go tool nm: %v", err)
297+
}
298+
if strings.Contains(string(buf), "MethodByName") {
299+
t.Error("compiled programs contains MethodByName symbol")
300+
}
301+
}

0 commit comments

Comments
 (0)