Skip to content

Instantly share code, notes, and snippets.

@syffer
Forked from klmr/Makefile
Last active March 29, 2021 12:52
Show Gist options
  • Save syffer/d1ff05f9f019bc4a763a27c441ca7c46 to your computer and use it in GitHub Desktop.
Save syffer/d1ff05f9f019bc4a763a27c441ca7c46 to your computer and use it in GitHub Desktop.
Self-documenting makefiles

What is it?

A make rule that allows pretty-printing short documentation for the rules inside a Makefile, which supports multi-line documentation, for either targets and variables (defined globally, via define, or via a target definition), and sorting by categories.

Example of resulting rendered help

$ make help
Usage: make [TARGET [TARGET ...]] [ARGUMENT=VALUE [ARGUMENT=VALUE ...]]
Targets and Arguments:
    cache-clear
        delete the cache
    help
        show the help
Tests:
    tests
        run the tests
    tests CACHE_RESULT (default: )
        cache the test results
    tests FILTERS (default: )
        execute only the tests matching the given filter
    tests STOP_ON_DEFECT (default: )
        stop testing at first defect / failure 

How to document?

A target or a variable can be documented by adding one or multiple ## before a target or a variable like so:

## a target which does something
target: 
    @echo "does something"
    
## a value used for disabling stuff
VARIABLE = value

A category can also be added to the documentation by using the tag @category . The all line will then be used as the category's name.

## @category A specific category
## a documentation
## on multiple lines
target-with-category:
    @echo "target on a category"

How does it work?

It uses 2 sed and 1 sort, and is made of 3 steps :

  1. The first sed is used to
    1. append the documentation after the target or the variable definition (thanks to klmr)
    2. parse the makefiles into an "html like" tag format. e.g. a variable's value would be prefixed and suffixed by <value> and </value>
    3. the categories are searched in the documentation comments, and is added at the begining of the line. If no category can be found, a default one is added
  2. The result is then sorted. Because the tags are "named" correctly, the sort command orders the lines like so
    1. the default category is place before any other categories
    2. the global variables
    3. the target
    4. the target's variables
  3. A final sed is then used to render the help in the wanted final format, by replacing the "tags" with indentation and colors

Limitations

Currently, it is necessary to add the @category tag to both targets and variables documentations in order for them to be categorized in the same group. It might be possible to avoid this by adding yet another sed after the first sort, which would regroup the targets and their variables on the same line, and extract the categories and putting them at the beginning of the line, for being re-sorted yet again.

Thanks

Based on a gist of klmr Which is based on an idea by @marmelab

# fancy colors
RULE_COLOR := "$$(tput setaf 6)"
VARIABLE_COLOR = "$$(tput setaf 2)"
VALUE_COLOR = "$$(tput setaf 1)"
CLEAR_STYLE := "$$(tput sgr0)"
TARGET_STYLED_HELP_NAME = "$(RULE_COLOR)TARGET$(CLEAR_STYLE)"
ARGUMENTS_HELP_NAME = "$(VARIABLE_COLOR)ARGUMENT$(CLEAR_STYLE)=$(VALUE_COLOR)VALUE$(CLEAR_STYLE)"
# search regex
target_regex = [a-zA-Z0-9%_\/%-]+
variable_regex = [^:=\s ]+
variable_assignment_regex = \s*:?[+:!\?]?=\s*
value_regex = .*
category_annotation_regex = @category\s+
category_regex = [^<]+
# tags used to delimit each parts
target_tag_start = "\<target-definition\>"
target_tag_end = "\<\\\/target-definition\>"
target_variable_tag_start = "\<target-variable\>"
target_variable_tag_end = "\<\\\/target-variable\>"
variable_tag_start = "\<variable\>"
variable_tag_end = "\<\\\/variable\>"
global_variable_tag_start = "\<global-variable\>"
global_variable_tag_end = "\<\\\/global-variable\>"
value_tag_start = "\<value\>"
value_tag_end = "\<\\\/value\>"
prerequisites_tag_start = "\<prerequisites\>"
prerequisites_tag_end = "\<\\\/prerequisites\>"
doc_tag_start = "\<doc\>"
doc_tag_end = "\<\\\/doc\>"
category_tag_start = "\<category-other\>"
category_tag_end = "\<\\\/category-other\>"
default_category_tag_start = "\<category-default\>"
default_category_tag_end = "\<\\\/category-default\>"
DEFAULT_CATEGORY = Targets and Arguments
## show the help
help:
@echo "Usage: make [$(TARGET_STYLED_HELP_NAME) [$(TARGET_STYLED_HELP_NAME) ...]] [$(ARGUMENTS_HELP_NAME) [$(ARGUMENTS_HELP_NAME) ...]]"
@sed -n -e "/^## / { \
h; \
s/.*/##/; \
:doc" \
-E -e "H; \
n; \
s/^##\s*(.*)/$(doc_tag_start)\1$(doc_tag_end)/; \
t doc" \
-e "s/\s*#[^#].*//; " \
-E -e "s/^(define\s*)?($(variable_regex))$(variable_assignment_regex)($(value_regex))/$(global_variable_tag_start)\2$(global_variable_tag_end)$(value_tag_start)\3$(value_tag_end)/;" \
-E -e "s/^($(target_regex))\s*:?:\s*(($(variable_regex))$(variable_assignment_regex)($(value_regex)))/$(target_variable_tag_start)\1$(target_variable_tag_end)$(variable_tag_start)\3$(variable_tag_end)$(value_tag_start)\4$(value_tag_end)/;" \
-E -e "s/^($(target_regex))\s*:?:\s*($(target_regex)(\s*$(target_regex))*)?/$(target_tag_start)\1$(target_tag_end)$(prerequisites_tag_start)\2$(prerequisites_tag_end)/;" \
-E -e " \
G; \
s/##\s*(.*)\s*##/$(doc_tag_start)\1$(doc_tag_end)/; \
s/\\n//g;" \
-E -e "/$(category_annotation_regex)/!s/.*/$(default_category_tag_start)$(DEFAULT_CATEGORY)$(default_category_tag_end)&/" \
-E -e "s/^(.*)$(doc_tag_start)$(category_annotation_regex)($(category_regex))$(doc_tag_end)/$(category_tag_start)\2$(category_tag_end)\1/" \
-e "p; \
}" ${MAKEFILE_LIST} \
| sort \
| sed -n \
-e "s/$(default_category_tag_start)/$(category_tag_start)/" \
-e "s/$(default_category_tag_end)/$(category_tag_end)/" \
-E -e "{G; s/($(category_tag_start)$(category_regex)$(category_tag_end))(.*)\n\1/\2/; s/\n.*//; H; }" \
-e "s/$(category_tag_start)//" \
-e "s/$(category_tag_end)/:\n/" \
-e "s/$(target_variable_tag_start)/$(target_tag_start)/" \
-e "s/$(target_variable_tag_end)/$(target_tag_end)/" \
-e "s/$(target_tag_start)/ $(RULE_COLOR)/" \
-e "s/$(target_tag_end)/$(CLEAR_STYLE) /" \
-e "s/$(prerequisites_tag_start)$(prerequisites_tag_end)//" \
-e "s/$(prerequisites_tag_start)/[/" \
-e "s/$(prerequisites_tag_end)/]/" \
-E -e "s/$(global_variable_tag_start)/ $(variable_tag_start)/g" \
-E -e "s/$(global_variable_tag_end)/$(variable_tag_end)/" \
-E -e "s/$(variable_tag_start)/$(VARIABLE_COLOR)/g" \
-E -e "s/$(variable_tag_end)/$(CLEAR_STYLE)/" \
-e "s/$(value_tag_start)/ (default: $(VALUE_COLOR)/" \
-e "s/$(value_tag_end)/$(CLEAR_STYLE))/" \
-e "s/$(doc_tag_start)/\n /g" \
-e "s/$(doc_tag_end)//g" \
-e "s/\r//g" \
-e "p"
include help.make
.DEFAULT_GOAL := help
## a simple target
## with multi-line documentation
target:
@echo "simple target"
## a target with some pattern rule
target-with-patter-rule-%: file-with-pattern-rule-%
## @category Variable
VARIABLE = "lazy variable"
## @category Variable
VARIABLE_APPENDING_VALUE += "append value"
## @category Variable
VARIABLE_SIMPLY_EXPANDED := "simply expanded"
## @category Variable
VARIABLE_SIMPLY_EXMAPLED_DOUBLE_COLON ::= "simply expanded double colon"
## @category Variable
VARIABLE_CONDITIONAL_ASSIGNMENT ?= "conditional assignment"
## @category Variable
VARIABLE_ASSIGNED_TO_SHELL != "assigned to shell"
## @category Variable
define VARIABLE_WITH_DEFINE :=
endef
## @category Complex Target
## a complex target with variable uses
complex-target: target
@echo "a complex target"
## @category Complex Target
complex-target: VARIABLE = "lazy variable"
## @category Complex Target
complex-target: VARIABLE_APPENDING_VALUE += "append value"
## @category Complex Target
complex-target: VARIABLE_SIMPLY_EXPANDED := "simply expanded"
## @category Complex Target
complex-target: VARIABLE_CONDITIONAL_ASSIGNMENT ?= "conditional assignment"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment