diff --git a/.github/workflows/check-automated-doc.yml b/.github/workflows/check-automated-doc.yml index ae07734cbc26..c3126789d533 100644 --- a/.github/workflows/check-automated-doc.yml +++ b/.github/workflows/check-automated-doc.yml @@ -46,10 +46,10 @@ jobs: - name: Verify golang generated documentation is up-to-date run: | - make generate-doc + make doc if [[ $(git diff) ]]; then echo "❌ fail: uncommited changes" - echo "please run 'make generate-doc' and commit the changes" + echo "please run 'make doc' and commit the changes" git --no-pager diff exit 1 fi diff --git a/.github/workflows/test-db-changes.yml b/.github/workflows/test-db-changes.yml index 10d1ec4ddea2..2a3ce5f6463d 100644 --- a/.github/workflows/test-db-changes.yml +++ b/.github/workflows/test-db-changes.yml @@ -105,9 +105,9 @@ jobs: - name: Verify test schema changes run: | - make dump-test-schema + make test-schema if [[ $(git diff server/datastore/mysql/schema.sql) ]]; then echo "❌ fail: uncommited changes in schema.sql" - echo "please run `make dump-test-schema` and commit the changes" + echo "please run `make test-schema` and commit the changes" exit 1 fi \ No newline at end of file diff --git a/Makefile b/Makefile index 23c7fc5c178c..e06bdda6eebf 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,8 @@ REVISION = $(shell git rev-parse HEAD) REVSHORT = $(shell git rev-parse --short HEAD) USER = $(shell whoami) DOCKER_IMAGE_NAME = fleetdm/fleet +# The tool that was called on the command line (probably `make` or `fdm`). +TOOL_CMD = "make" ifdef GO_BUILD_RACE_ENABLED GO_BUILD_RACE_ENABLED_VAR := true @@ -59,47 +61,6 @@ LDFLAGS_VERSION = "\ all: build -define HELP_TEXT - - Makefile commands - - make deps - Install dependent programs and libraries - make generate - Generate and bundle required all code - make generate-go - Generate and bundle required go code - make generate-js - Generate and bundle required js code - make generate-dev - Generate and bundle required code in a watch loop - - make migration - create a database migration file (supply name=TheNameOfYourMigration) - - make generate-doc - Generate updated API documentation for activities, osquery flags - make dump-test-schema - update schema.sql from current migrations - make generate-mock - update mock data store - - make clean - Clean all build artifacts - make clean-assets - Clean assets only - - make build - Build the code - make package - Build rpm and deb packages for linux - - make run-go-tests - Run Go tests in specific packages - make debug-go-tests - Debug Go tests in specific packages (with Delve) - make test-js - Run the JavaScript tests - - make lint - Run all linters - make lint-go - Run the Go linters - make lint-js - Run the JavaScript linters - make lint-scss - Run the SCSS linters - make lint-ts - Run the TypeScript linters - - For use in CI: - - make test - Run the full test suite (lint, Go and Javascript) - make test-go - Run the Go tests (all packages and tests) - -endef - -help: - $(info $(HELP_TEXT)) .prefix: mkdir -p build/linux @@ -115,7 +76,41 @@ help: .pre-fleetctl: $(eval APP_NAME = fleetctl) -build: fleet fleetctl +# For the build target, decide which binaries to build. +BINS_TO_BUILD = +ifeq (build,$(filter build,$(MAKECMDGOALS))) + BINS_TO_BUILD = fleet fleetctl + ifeq ($(ARG1), fleet) + BINS_TO_BUILD = fleet + else ifeq ($(ARG1), fleetctl) + BINS_TO_BUILD = fleetctl + endif +endif +.help-short--build: + @echo "Build binaries" +.help-long--build: + @echo "Builds the specified binaries (defaults to building fleet and fleetctl)" +.help-usage--build: + @echo "$(TOOL_CMD) build [binaries] [options]" +.help-options--build: + @echo "GO_BUILD_RACE_ENABLED" + @echo "Turn on data race detection when building" + @echo "EXTRA_FLEETCTL_LDFLAGS=\"--flag1 --flag2...\"" + @echo "Flags to provide to the Go linker when building fleetctl" +.help-extra--build: + @echo "AVAILABLE BINARIES:" + @echo " fleet Build the fleet binary" + @echo " fleetctl Build the fleetctl binary" +build: $(BINS_TO_BUILD) + +.help-short--fdm: + @echo "Builds the fdm command" +fdm: + go build -o build/fdm ./tools/fdm + @if [ ! -f /usr/local/bin/fdm ]; then \ + echo "Linking to /usr/local/bin/fdm..."; \ + sudo ln -sf "$$(pwd)/build/fdm" /usr/local/bin/fdm; \ + fi fleet: .prefix .pre-build .pre-fleet CGO_ENABLED=1 go build -race=${GO_BUILD_RACE_ENABLED_VAR} -tags full,fts5,netgo -o build/${OUTPUT} -ldflags ${LDFLAGS_VERSION} ./cmd/fleet @@ -132,50 +127,51 @@ fleetctl: .prefix .pre-build .pre-fleetctl fleetctl-dev: GO_BUILD_RACE_ENABLED_VAR=true fleetctl-dev: fleetctl +.help-short--lint-js: + @echo "Run the JavaScript linters" lint-js: yarn lint +.help-short--lint-go: + @echo "Run the Go linters" lint-go: golangci-lint run --exclude-dirs ./node_modules --timeout 15m +.help-short--lint: + @echo "Run linters" +.help-long--lint: + @echo "Runs the linters for Go and Javascript code. If linter type is not specified, all linters will be run." +.help-usage--lint: + @echo "$(TOOL_CMD) lint [linter-type]" +.help-extra--lint: + @echo "AVAILABLE LINTERS:" + @echo " go Lint Go files with golangci-lint" + @echo " js Lint .js, .jsx, .ts and .tsx files with eslint" + +ifdef ARG1 +lint: lint-$(ARG1) +else lint: lint-go lint-js +endif -dump-test-schema: +.help-short--test-schema: + @echo "Update schema.sql from current migrations" +test-schema: go run ./tools/dbutils ./server/datastore/mysql/schema.sql - +dump-test-schema: test-schema # This is the base command to run Go tests. # Wrap this to run tests with presets (see `run-go-tests` and `test-go` targets). # PKG_TO_TEST: Go packages to test, e.g. "server/datastore/mysql". Separate multiple packages with spaces. # TESTS_TO_RUN: Name specific tests to run in the specified packages. Leave blank to run all tests in the specified packages. # GO_TEST_EXTRA_FLAGS: Used to specify other arguments to `go test`. -# GO_TEST_MAKE_FLAGS: Internal var used by other targets to add arguments to `go test`. -# -PKG_TO_TEST := "" # default to empty string; can be overridden on command line. +# GO_TEST_MAKE_FLAGS: Internal var used by other targets to add arguments to `go test`. +PKG_TO_TEST := "" go_test_pkg_to_test := $(addprefix ./,$(PKG_TO_TEST)) # set paths for packages to test dlv_test_pkg_to_test := $(addprefix github.com/fleetdm/fleet/v4/,$(PKG_TO_TEST)) # set URIs for packages to debug - -DEFAULT_PKG_TO_TEST := ./cmd/... ./ee/... ./orbit/pkg/... ./orbit/cmd/orbit ./pkg/... ./server/... ./tools/... -ifeq ($(CI_TEST_PKG), main) - CI_PKG_TO_TEST=$(shell go list ${DEFAULT_PKG_TO_TEST} | grep -v "server/datastore/mysql" | grep -v "cmd/fleetctl" | grep -v "server/vulnerabilities" | sed -e 's|github.com/fleetdm/fleet/v4/||g') -else ifeq ($(CI_TEST_PKG), integration) - CI_PKG_TO_TEST="server/service" -else ifeq ($(CI_TEST_PKG), mysql) - CI_PKG_TO_TEST="server/datastore/mysql/..." -else ifeq ($(CI_TEST_PKG), fleetctl) - CI_PKG_TO_TEST="cmd/fleetctl/..." -else ifeq ($(CI_TEST_PKG), vuln) - CI_PKG_TO_TEST="server/vulnerabilities/..." -else - CI_PKG_TO_TEST=$(DEFAULT_PKG_TO_TEST) -endif - -ci-pkg-list: - @echo $(CI_PKG_TO_TEST) - .run-go-tests: ifeq ($(PKG_TO_TEST), "") - @echo "Please specify one or more packages to test with argument PKG_TO_TEST=\"/path/to/pkg/1 /path/to/pkg/2\"..."; + @echo "Please specify one or more packages to test. See '$(TOOL_CMD) help run-go-tests' for more info."; else @echo Running Go tests with command: go test -tags full,fts5,netgo -run=${TESTS_TO_RUN} ${GO_TEST_MAKE_FLAGS} ${GO_TEST_EXTRA_FLAGS} -parallel 8 -coverprofile=coverage.txt -covermode=atomic -coverpkg=github.com/fleetdm/fleet/v4/... $(go_test_pkg_to_test) @@ -183,46 +179,103 @@ endif # This is the base command to debug Go tests. # Wrap this to run tests with presets (see `debug-go-tests`) -# PKG_TO_TEST: Go packages to test, e.g. "server/datastore/mysql". Separate multiple packages with spaces. -# TESTS_TO_RUN: Name specific tests to debug in the specified packages. Leave blank to debug all tests in the specified packages. # DEBUG_TEST_EXTRA_FLAGS: Internal var used by other targets to add arguments to `dlv test`. -# GO_TEST_EXTRA_FLAGS: Used to specify other arguments to `go test`. .debug-go-tests: ifeq ($(PKG_TO_TEST), "") - @echo "Please specify one or more packages to debug with argument PKG_TO_TEST=\"/path/to/pkg/1 /path/to/pkg/2\"..."; + @echo "Please specify one or more packages to debug. See '$(TOOL_CMD) help run-go-tests' for more info."; else @echo Debugging tests with command: dlv test ${dlv_test_pkg_to_test} --api-version=2 --listen=127.0.0.1:61179 ${DEBUG_TEST_EXTRA_FLAGS} -- -test.v -test.run=${TESTS_TO_RUN} ${GO_TEST_EXTRA_FLAGS} endif -# Command to run specific tests in development. Can run all tests for one or more packages, or specific tests within packages. +.help-short--run-go-tests: + @echo "Run Go tests in specific packages" +.help-long--run-go-tests: + @echo Command to run specific tests in development. Can run all tests for one or more packages, or specific tests within packages. +.help-options--run-go-tests: + @echo "PKG_TO_TEST=\"pkg1 pkg2...\"" + @echo "Go packages to test, e.g. \"server/datastore/mysql\". Separate multiple packages with spaces." + @echo "TESTS_TO_RUN=\"test\"" + @echo Name specific tests to debug in the specified packages. Leave blank to debug all tests in the specified packages. + @echo "GO_TEST_EXTRA_FLAGS=\"--flag1 --flag2...\"" + @echo "Arguments to send to \"go test\"." run-go-tests: @MYSQL_TEST=1 REDIS_TEST=1 MINIO_STORAGE_TEST=1 SAML_IDP_TEST=1 NETWORK_TEST=1 make .run-go-tests GO_TEST_MAKE_FLAGS="-v" +.help-short--debug-go-tests: + @echo "Debug Go tests in specific packages (with Delve)" +.help-long--debug-go-tests: + @echo Command to run specific tests in the Go debugger. Can run all tests for one or more packages, or specific tests within packages. +.help-options--debug-go-tests: + @echo "PKG_TO_TEST=\"pkg1 pkg2...\"" + @echo "Go packages to test, e.g. \"server/datastore/mysql\". Separate multiple packages with spaces." + @echo "TESTS_TO_RUN=\"test\"" + @echo Name specific tests to debug in the specified packages. Leave blank to debug all tests in the specified packages. + @echo "GO_TEST_EXTRA_FLAGS=\"--flag1 --flag2...\"" + @echo "Arguments to send to \"go test\"." debug-go-tests: @MYSQL_TEST=1 REDIS_TEST=1 MINIO_STORAGE_TEST=1 SAML_IDP_TEST=1 NETWORK_TEST=1 make .debug-go-tests +# Set up packages for CI testing. +DEFAULT_PKG_TO_TEST := ./cmd/... ./ee/... ./orbit/pkg/... ./orbit/cmd/orbit ./pkg/... ./server/... ./tools/... +ifeq ($(CI_TEST_PKG), main) + CI_PKG_TO_TEST=$(shell go list ${DEFAULT_PKG_TO_TEST} | grep -v "server/datastore/mysql" | grep -v "cmd/fleetctl" | grep -v "server/vulnerabilities" | sed -e 's|github.com/fleetdm/fleet/v4/||g') +else ifeq ($(CI_TEST_PKG), integration) + CI_PKG_TO_TEST="server/service" +else ifeq ($(CI_TEST_PKG), mysql) + CI_PKG_TO_TEST="server/datastore/mysql/..." +else ifeq ($(CI_TEST_PKG), fleetctl) + CI_PKG_TO_TEST="cmd/fleetctl/..." +else ifeq ($(CI_TEST_PKG), vuln) + CI_PKG_TO_TEST="server/vulnerabilities/..." +else + CI_PKG_TO_TEST=$(DEFAULT_PKG_TO_TEST) +endif # Command used in CI to run all tests. -test-go: dump-test-schema generate-mock +.help-short--test-go: + @echo "Run Go tests for CI" +.help-long--test-go: + @echo "Run one or more bundle of Go tests. These are bundled together to try and make CI testing more parallelizable (and thus faster)." +.help-options--test-go: + @echo "CI_TEST_PKG=[test package]" + @echo "The test package bundle to run. If not specified, all Go tests will run." +.help-extra--test-go: + @echo "AVAILABLE TEST BUNDLES:" + @echo " integration" + @echo " mysql" + @echo " fleetctl" + @echo " vuln" + @echo " main (all tests not included in other bundles)" +test-go: test-schema mock make .run-go-tests PKG_TO_TEST="$(CI_PKG_TO_TEST)" analyze-go: go test -tags full,fts5,netgo -race -cover ./... +.help-short--test-js: + @echo "Run the JavaScript tests" test-js: yarn test +.help-short--test: + @echo "Run the full test suite (lint, Go and Javascript -- used in CI)" test: lint test-go test-js +.help-short--generate: + @echo "Generate and bundle required Go code and Javascript code" generate: clean-assets generate-js generate-go generate-ci: NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=development yarn run webpack make generate-go +.help-short--generate-js: + @echo "Generate and bundle required js code" generate-js: clean-assets .prefix NODE_ENV=production yarn run webpack --progress +.help-short--generate-go: + @echo "Generate and bundle required go code" generate-go: .prefix go run github.com/kevinburke/go-bindata/go-bindata -pkg=bindata -tags full \ -o=server/bindata/generated.go \ @@ -231,6 +284,8 @@ generate-go: .prefix # we first generate the webpack bundle so that bindata knows to atch the # output bundle file. then, generate debug bindata source file. finally, we # run webpack in watch mode to continuously re-generate the bundle +.help-short--generate-dev: + @echo "Generate and bundle required Javascript code in a watch loop" generate-dev: .prefix NODE_ENV=development yarn run webpack --progress go run github.com/kevinburke/go-bindata/go-bindata -debug -pkg=bindata -tags full \ @@ -238,13 +293,21 @@ generate-dev: .prefix frontend/templates/ assets/... server/mail/templates NODE_ENV=development yarn run webpack --progress --watch -generate-mock: .prefix +.help-short--mock: + @echo "Update mock data store" +mock: .prefix go generate github.com/fleetdm/fleet/v4/server/mock github.com/fleetdm/fleet/v4/server/mock/mockresult github.com/fleetdm/fleet/v4/server/service/mock +generate-mock: mock -generate-doc: .prefix +.help-short--doc: + @echo "Generate updated API documentation for activities, osquery flags" +doc: .prefix go generate github.com/fleetdm/fleet/v4/server/fleet go generate github.com/fleetdm/fleet/v4/server/service/osquery_utils +generate-doc: doc +.help-short--deps: + @echo "Install dependent programs and libraries" deps: deps-js deps-go deps-js: @@ -263,14 +326,20 @@ check-go-cloner: update-go-cloner: go run ./tools/cloner-check/main.go --update +.help-short--migration: + @echo "Create a database migration file (supply name=TheNameOfYourMigration)" migration: go run ./server/goose/cmd/goose -dir server/datastore/mysql/migrations/tables create $(name) gofmt -w server/datastore/mysql/migrations/tables/*_$(name)*.go +.help-short--clean: + @echo "Clean all build artifacts" clean: clean-assets rm -rf build vendor rm -f assets/bundle.js +.help-short--clean-assets: + @echo "Clean assets only" clean-assets: git clean -fx assets @@ -600,3 +669,8 @@ db-replica-reset: fleet # db-replica-run runs fleet serve with one main and one read MySQL instance. db-replica-run: fleet FLEET_MYSQL_ADDRESS=127.0.0.1:3308 FLEET_MYSQL_READ_REPLICA_ADDRESS=127.0.0.1:3309 FLEET_MYSQL_READ_REPLICA_USERNAME=fleet FLEET_MYSQL_READ_REPLICA_DATABASE=fleet FLEET_MYSQL_READ_REPLICA_PASSWORD=insecure ./build/fleet serve --dev --dev_license + +include ./tools/makefile-support/helpsystem-targets + +foo: + @echo $(MAKECMDGOALS) \ No newline at end of file diff --git a/tools/fdm/main.go b/tools/fdm/main.go new file mode 100644 index 000000000000..ce24717f8647 --- /dev/null +++ b/tools/fdm/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" +) + +func main() { + // Ensure there's a make target specified. + if len(os.Args) < 2 { + fmt.Println("Usage: fdm [--option=value ...] -- [make-options]") + os.Exit(1) + } + + // Determine the path to the top-level directory (where the Makefile resides). + repoRoot, err := getRepoRoot() + if err != nil { + fmt.Printf("Error determining repo root: %v\n", err) + os.Exit(1) + } + + // Change the working directory to the repo root. + if err := os.Chdir(repoRoot); err != nil { + fmt.Printf("Error changing directory to repo root: %v\n", err) + os.Exit(1) + } + + // Extract the make target. + makeTarget := os.Args[1] + + // Split arguments into options and make arguments. + options, makeArgs := splitArgs(os.Args[2:]) + + // Special logic for the help command. + if makeTarget == "help" { + if len(os.Args) > 2 && !strings.HasPrefix(os.Args[2], "--") { + options["REFORMAT_OPTIONS"] = "true" + } else { + fmt.Println("\033[1mNAME\033[0m") + fmt.Println(" fdm - developer tools for fleet device management") + fmt.Println() + fmt.Println("\033[1mUSAGE:\033[0m") + fmt.Println(" fdm [--option=value ...] -- [make-options]") + fmt.Println() + fmt.Println("\033[1mCOMMANDS:\033[0m") + options["HELP_CMD_PREFIX"] = "fdm" + } + } + + // Transform options into Makefile-compatible variables. + makeVars := transformToMakeVars(options) + makeVars = append(makeVars, "TOOL_CMD=fdm") + + // Call the Makefile with the specified target, Make variables, and additional arguments. + err = callMake(makeTarget, makeVars, makeArgs) + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} + +// splitArgs splits the arguments into options and make arguments based on the `--` delimiter. +func splitArgs(args []string) (map[string]string, []string) { + options := make(map[string]string) + var makeArgs []string + positionalArgsIndex := 1 + isMakeArgs := false + skipNext := false + + for idx, arg := range args { + if skipNext { + skipNext = false + continue + } + + if arg == "--" { + isMakeArgs = true + continue + } + + switch { + // If we're processing make args (anything after a bare -- ) + // then add the current arg to the list. + case isMakeArgs: + makeArgs = append(makeArgs, arg) + // Otherwise if the arg has a -- prefix, treat it like an option + // for the command. + case strings.HasPrefix(arg, "--"): + // Remove "--" and split by "=". + parts := strings.SplitN(arg[2:], "=", 2) + switch { + // Handle options like --name=foo + case len(parts) == 2: + options[parts[0]] = parts[1] + // Handle options like --name foo + case idx+1 < len(args) && !strings.HasPrefix(args[idx+1], "--"): + options[arg[2:]] = args[idx+1] + skipNext = true + // Handle options like --useturbocharge by assuming they're booleans. + default: + options[parts[0]] = "true" + } + // Otherwise assume we're dealing with a positional argument. + default: + options["arg"+strconv.Itoa(positionalArgsIndex)] = arg + positionalArgsIndex++ + } + } + + return options, makeArgs +} + +// transformToMakeVars converts kebab-cased options to snake-cased Makefile variables. +func transformToMakeVars(options map[string]string) []string { + var makeVars []string + + for key, value := range options { + // Convert kebab-case to snake_case and uppercase. + varName := strings.ToUpper(strings.ReplaceAll(key, "-", "_")) + makeVars = append(makeVars, fmt.Sprintf("%s=%s", varName, value)) + } + + return makeVars +} + +// callMake invokes the `make` command with the given target, variables, and additional arguments. +func callMake(target string, makeVars []string, makeArgs []string) error { + // Construct the command with target and makeArgs. + finalArgs := []string{target} + finalArgs = append(finalArgs, makeVars...) + finalArgs = append(finalArgs, makeArgs...) + cmd := exec.Command("make", finalArgs...) + + // Use the same stdin, stdout, and stderr as the parent process. + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Run the command. + return cmd.Run() +} + +// getRepoRoot determines the repo root (top-level directory) relative to this binary. +func getRepoRoot() (string, error) { + // Get the path of the currently executing binary + executable, err := os.Executable() + if err != nil { + return "", err + } + + // Get the path of the binary, following symlinks. + execDir, err := filepath.EvalSymlinks(executable) + if err != nil { + return "", err + } + // Get the directory. + execDir = filepath.Dir(execDir) + + // Compute the repo root relative to the binary's location. + repoRoot := filepath.Join(execDir, "../") + return filepath.Abs(repoRoot) +} diff --git a/tools/makefile-support/helpsystem-targets b/tools/makefile-support/helpsystem-targets new file mode 100644 index 000000000000..c235ced6e944 --- /dev/null +++ b/tools/makefile-support/helpsystem-targets @@ -0,0 +1,20 @@ +# Default prefix for commands in the main help command, e.g. "make build". +# The FDM utility may change this to "fdm". +HELP_CMD_PREFIX ?= make +CMD = $(ARG1) + +# Run the main help command. +help: + @CMD=$(CMD) REFORMAT_OPTIONS=$(REFORMAT_OPTIONS) HELP_CMD_PREFIX=$(HELP_CMD_PREFIX) ./tools/makefile-support/makehelp.sh + +# Print separators between help sections. +.help-sep-1: + @printf "\036" +.help-sep-2: + @printf "\036" +.help-sep-3: + @printf "\036" +.help-sep-4: + @printf "\036" +.help-sep-5: + @printf "\036" diff --git a/tools/makefile-support/makehelp.sh b/tools/makefile-support/makehelp.sh new file mode 100755 index 000000000000..2b3743090958 --- /dev/null +++ b/tools/makefile-support/makehelp.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# If a command was specified like `make help CMD=build` then try to +# gather help for that command. +if [ -n "$CMD" ]; then + # Get the make targets for generating different help sections + short_target=".help-short--$CMD"; + long_target=".help-long--$CMD"; + options_target=".help-options--$CMD"; + usage_target=".help-usage--$CMD"; + extra_target=".help-extra--$CMD"; + + delim=$'\036' # ASCII 30 + nl=$'\037' + + # Try and get the help for the command. Sections of the output will be delimited bv ASCII 30 (arbirary non-printing char) + output=$(make -k $short_target .help-sep-1 $long_target .help-sep-2 $options_target .help-sep-3 $usage_target .help-sep-4 $extra_target .help-sep-5 2>/dev/null) + # Clean the output for "read" by replacing newlines with ASCII 31 (also arbitrary) + cleaned_output=$(echo "$output" | tr '\n' $nl ) + # Read the output into an array + IFS="$delim" read -r -a sections <<<"$cleaned_output" + # Get the newlines back + short_desc="${sections[0]}" + long_desc=$(echo "${sections[1]}" | tr $nl '\n') + options_text=$(echo "${sections[2]}" | tr $nl '\n') + usage_text=$(echo "${sections[3]}" | tr $nl '\n') + extra_text=$(echo "${sections[4]}" | tr $nl '\n') + + # If we found a long help description, then continue printing help. + if [ -n "$long_desc" ]; then + # Print a loading message since make takes a second to run. + echo -n "Gathering help for $CMD command..."; + # If this command has options, output them as well. + if [ -n "$options_text" ]; then + # The REFORMAT_OPTIONS flag turns makefile options like DO_THE_THING into + # CLI options like --do-the-thing. + if [ -n "$REFORMAT_OPTIONS" ]; then + options_text=$(paste -s -d '\t\n' <(echo "$options_text" | awk 'NR % 2 == 1 { option = $0; gsub("_", "-", option); printf " --%s\n", tolower(option); next } { print $0 }') | column -t -s $'\t'); + else + options_text=$(paste -s -d '\t\n' <(echo "$options_text") | column -t -s $'\t'); + fi; + fi; + # We're done loading, so erase the loading message. + echo -ne "\r\033[K"; + # Output whatever help we hot. + echo -e "\033[1mNAME:\033[0m"; + echo " $CMD - $short_desc"; + if [ -n "$usage_text" ]; then + echo; + echo -e "\033[1mUSAGE:\033[0m"; + echo " $usage_text"; + fi; + if [ -n "$long_desc" ]; then + echo; + echo -e "\033[1mDESCRIPTION:\033[0m"; + echo " $long_desc" | fmt -w 80; + fi; + if [ -n "$options_text" ]; then + echo; + echo -e "\033[1mOPTIONS:\033[0m"; + echo "$options_text"; + fi; + if [ -n "$extra_text" ]; then + IFS= read -r first_line <<< "$extra_text" + rest_of_text="$(echo "$extra_text" | tail -n +2)" + echo; + echo -e "\033[1m$first_line\033[0m" + echo "$rest_of_text"; + fi; + # If there's no long help description, there's no additional help for the command. + else + echo "No help found for $CMD command."; + fi; + +# If no specific help was requested, output all the available commands. +else + targets=$(awk '/^[^#[:space:]].*:/ {print $1}' Makefile | grep '^\.help-short--' | sed 's/:$//' | sort); + if [ -n "$targets" ]; then + output=$(make --no-print-directory $targets 2>/dev/null); + paste <(echo "$targets" | sed "s/^\.help-short--/ $HELP_CMD_PREFIX /") <(echo "$output") | column -t -s $'\t'; echo; + else + echo "No help targets found."; + fi +fi + +