@brnshkr/config 0.0.1-beta.2 → 0.0.1-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +177 -228
  2. package/conf/.gitignore.dist +15 -0
  3. package/conf/Makefile +3778 -0
  4. package/conf/Makefile.dist +13 -0
  5. package/conf/bunfig.dist.toml +5 -0
  6. package/conf/commitlint.dist.mjs +1 -0
  7. package/conf/editorconfig.dist +19 -0
  8. package/conf/launch.dist.json +31 -0
  9. package/conf/markdownlint.dist.mjs +1 -0
  10. package/conf/spelling/defaults.json +185 -0
  11. package/conf/tsconfig.dist.json +3 -0
  12. package/conf/tsconfig.json +31 -27
  13. package/conf/vitest.dist.mjs +1 -0
  14. package/conf/vscode-css-custom-data.dist.json +95 -0
  15. package/conf/vscode-extensions.dist.json +21 -0
  16. package/conf/vscode-settings.dist.jsonc +338 -0
  17. package/dist/commitlint/index.d.mts +72 -0
  18. package/dist/commitlint/index.mjs +239 -0
  19. package/dist/eslint/index.d.mts +3897 -1383
  20. package/dist/eslint/index.mjs +2784 -419
  21. package/dist/markdownlint/index.d.mts +2304 -0
  22. package/dist/markdownlint/index.mjs +440 -0
  23. package/dist/shared.mjs +405 -65
  24. package/dist/spelling/index.d.mts +30 -0
  25. package/dist/spelling/index.mjs +2 -0
  26. package/dist/spelling/spelling.test.d.mts +1 -0
  27. package/dist/spelling/spelling.test.mjs +12 -0
  28. package/dist/stylelint/index.d.mts +47 -9
  29. package/dist/stylelint/index.mjs +145 -47
  30. package/dist/vitest/index.d.mts +73 -0
  31. package/dist/vitest/index.mjs +181 -0
  32. package/package.json +192 -105
  33. package/conf/tsconfig.json.example +0 -3
  34. package/dist/scripts/eslint.mjs +0 -16
  35. package/dist/scripts/stylelint.mjs +0 -29
  36. /package/conf/{eslint.config.mjs.example → eslint.dist.mjs} +0 -0
  37. /package/conf/{stylelint.config.mjs.example → stylelint.dist.mjs} +0 -0
package/conf/Makefile ADDED
@@ -0,0 +1,3778 @@
1
+ #==
2
+ # brnshkr/config Makefile
3
+ # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
4
+ # This Makefile provides shared tooling, conventions, and utilities that can be
5
+ # included and extended by project-specific Makefiles. It implements a small
6
+ # framework around GNU Make to provide:
7
+ # - structured CLI-style commands
8
+ # - argument forwarding to targets
9
+ # - automatic help generation
10
+ # - terminal formatting helpers
11
+ #
12
+ # Variables and macros prefixed with an underscore ('_') are considered private
13
+ # implementation details and are not part of the public API.
14
+ #
15
+ # Macros that are written in UPPER_SNAKE_CASE are treated as variables.
16
+ # Macros that are written in lower_snake_case are treated as functions.
17
+ #
18
+ # This Makefile implements a custom argument parsing system that allows
19
+ # arguments to be passed to specific targets while preserving Make's standard behavior:
20
+ #
21
+ # Rules:
22
+ # - Arguments that do NOT match an existing target are treated as arguments
23
+ # - Arguments before the first valid target are passed to ALL invoked targets
24
+ # - Arguments between two targets are passed only to the preceding target
25
+ # - Runs that contain no valid targets trigger the error defined in .DEFAULT target
26
+ # - Arguments shaped 'name=value' are taken by make itself, so an option's value is written with a space
27
+ # - Make claims a dash-prefixed word as its own option, so flags follow a '--' separator
28
+ #
29
+ # Examples of what the above-mentioned looks like:
30
+ # make a -> runs target a
31
+ # make a b -> runs targets a and b
32
+ # make a a1 a2 b b1 b2 -> runs target a with arguments a1 and a2 and target b with arguments b1 and b2 (assuming a1, a2, b1 and b2 are not targets by themselves)
33
+ # make ab1 ab2 a a1 b -> runs target a with arguments ab1, ab2 and a1 and target b with arguments ab1 and ab2 (assuming ab1, ab2 and a1 are not targets by themselves)
34
+ # make no arg exists -> throws error defined in .DEFAULT target
35
+ #
36
+ # Any target can be prepended with the DEBUG flag to echo the commands as they run,
37
+ # or the TRACE flag to echo the guards along with them:
38
+ # - make DEBUG=1 phpstan
39
+ # - DEBUG=1 make phpstan
40
+ #
41
+ # A flag is off when it is empty or one of '0', 'false', 'off' and 'no', and on for any other value.
42
+ #
43
+ # All make runs can be prepend with the NO_ANSI flag to disable any ANSI escape sequences in the output:
44
+ # - make NO_ANSI=1 help
45
+ # - NO_ANSI=1 make help
46
+ #
47
+ # The `make help` command automatically generates documentation based on specially formatted comments:
48
+ # Scope headers (control grouping and nesting in the help output):
49
+ # #-- name — top-level group (depth 0); all subsequent symbols belong to this group
50
+ # #--- name — section (depth 1, nested inside the nearest depth-0 group)
51
+ # #---- name — subsection (depth 2)
52
+ # #-----… name — subsection (depth N)
53
+ #
54
+ # Append 'v' characters to set a minimum verbosity level for everything inside the scope.
55
+ # Arbitrary depth is supported — each additional 'v' raises the threshold by one:
56
+ # #---v name — contents hidden unless verbosity >= 1
57
+ # #---vv name — contents hidden unless verbosity >= 2
58
+ # #---vvv name — contents hidden unless verbosity >= 3
59
+ # (and so on)
60
+ #
61
+ # Inline description comments (attach metadata to a specific symbol):
62
+ # VARIABLE ?= value#~~ description — annotate a variable with a description
63
+ # command: #~~ description — annotate a command with a description
64
+ #
65
+ # Append 'v' characters after '#' to make the line itself require higher verbosity (also arbitrary depth):
66
+ # command: #~~ description #v — hide the command entry unless verbosity >= 1
67
+ # VARIABLE ?= value#vv #~~ … — hide the variable entry unless verbosity >= 2
68
+ # VARIABLE ?= value#~~ … #vvv — hide the variable entry unless verbosity >= 3
69
+ #
70
+ # Description block (shown at the top of the help output under "Description:"):
71
+ # #!! text — emit a free-form description line; multiple lines are supported
72
+ #
73
+ # Documentation blocks (shown for function macros at higher verbosities):
74
+ # #** — open a documentation block
75
+ # #* text — body line inside the block (leading '#* ' is stripped)
76
+ # The first non-comment, non-empty line after the block must be a 'define' statement.
77
+ #
78
+ # Filtering by scope(s) is possible:
79
+ # make help brnshkr
80
+ # make help brnshkr.phpstan
81
+ # make help brnshkr.phpstan brnshkr.rector
82
+ # make help brnshkr app.foo -- -v
83
+ #
84
+ # Selecting verbosity at the command line (arbitrary depth):
85
+ # make help v — verbosity 1
86
+ # make help vvv — verbosity 3
87
+ # make help vvvv — verbosity 4
88
+ # make help -- -vv — verbosity 2 (alias form)
89
+ # make help V=10 — verbosity 10 (numeric form via env var)
90
+ #
91
+ # Listing the scope dot-paths with something to show at the chosen verbosity:
92
+ # make help list-scopes
93
+ # make help ls
94
+ # make help ls vvv
95
+ # make help ls brnshkr
96
+ # make help ls brnshkr.theming app
97
+ #
98
+ # Printing what each variable expands to instead of its source text:
99
+ # make help resolve
100
+ # make help r
101
+ #
102
+ # Header overrides (LOGO, PACKAGE, VERSION, EDITOR):
103
+ # <VAR> = <unset> — fall back to the default (built-in logo, auto-detected editor)
104
+ # <VAR> = <value> — use the given value (EDITOR rejects values outside the supported list)
105
+ # <VAR> = — disable the feature (no logo, no clickable hyperlinks)
106
+ #
107
+ # EDITOR is the standard shell variable, so one inherited from the environment naming anything else
108
+ # is ignored rather than rejected; only a value set in a Makefile or on the command line is an error.
109
+ #==
110
+
111
+ #-- brnshkr
112
+
113
+ #!! Shared targets from the `brnshkr/config` package
114
+
115
+ _BRNSHKR_CONFIG_MAKEFILE := $(lastword $(MAKEFILE_LIST))
116
+ _BRNSHKR_CONFIG_DIR := $(patsubst %/,%,$(dir $(_BRNSHKR_CONFIG_MAKEFILE)))
117
+
118
+ _ARG_WORDS = $(patsubst -%,%,$(patsubst --%,%,$(_ARGS)))
119
+
120
+ _HELP_VERBOSITY_ARGS = $(strip $(foreach ARG_WORD,$(_ARG_WORDS),$(if $(subst v,,$(ARG_WORD)),,$(ARG_WORD))))
121
+ _HELP_VERBOSITY = $(words $(subst v,v ,$(lastword $(sort $(_HELP_VERBOSITY_ARGS)))))
122
+ _HELP_WORDS := list-scopes ls resolve r
123
+ _HELP_RESERVED_ARGS = $(_HELP_WORDS) $(_HELP_VERBOSITY_ARGS)
124
+ _HELP_SCOPES = $(strip $(filter-out $(_HELP_RESERVED_ARGS),$(_ARG_WORDS)))
125
+ _HAS_LIST_SCOPES_ARGUMENT = $(if $(filter list-scopes ls,$(_ARG_WORDS)),1)
126
+ _HAS_RESOLVE_ARGUMENT = $(if $(filter resolve r,$(_ARG_WORDS)),1)
127
+
128
+ _DEFAULT_GOAL := $(.DEFAULT_GOAL)
129
+
130
+ #--- includes
131
+
132
+ define _include_makefiles_in_directory
133
+ $(eval include $(filter-out $(abspath $(MAKEFILE_LIST)),$(wildcard $1/Makefile $1/*.mk)))
134
+ endef
135
+
136
+ _SEARCH_BASES := \
137
+ conf \
138
+ .local \
139
+ .local/conf
140
+
141
+ _INCLUDE_DIRECTORIES := $(CURDIR) \
142
+ $(foreach BASE,$(_SEARCH_BASES), \
143
+ $(CURDIR)/$(BASE) \
144
+ $(CURDIR)/$(BASE)/make \
145
+ )
146
+
147
+ $(foreach INCLUDE_DIRECTORY,$(_INCLUDE_DIRECTORIES), \
148
+ $(call _include_makefiles_in_directory,$(INCLUDE_DIRECTORY)) \
149
+ )
150
+
151
+ _CONSUMER_MAKEFILES := $(filter-out $(_BRNSHKR_CONFIG_MAKEFILE),$(MAKEFILE_LIST))
152
+
153
+ #---vvv tools
154
+
155
+ SHELL := $(if $(filter-out default,$(origin SHELL)),$(SHELL),/bin/sh)#~~ shell the recipes run in
156
+ CP ?= cp#~~ path to `cp` binary
157
+ CAT ?= cat#~~ path to `cat` binary
158
+ RM := $(if $(filter-out default,$(origin RM)),$(RM),rm)#~~ path to `rm` binary
159
+ MKDIR ?= mkdir#~~ path to `mkdir` binary
160
+ MV ?= mv#~~ path to `mv` binary
161
+ CMP ?= cmp#~~ path to `cmp` binary
162
+ CHMOD ?= chmod#~~ path to `chmod` binary
163
+ LN ?= ln#~~ path to `ln` binary
164
+ PRINTF ?= printf#~~ path to `printf` binary
165
+ AWK ?= awk#~~ path to `awk` binary
166
+ PHP ?= $(_RUN) php#~~ command that runs `php`
167
+ TAR ?= tar#~~ path to `tar` binary
168
+ GIT ?= git#~~ path to `git` binary
169
+ MKTEMP ?= mktemp#~~ path to `mktemp` binary
170
+ SCRIPT ?= script#~~ path to `script` binary
171
+
172
+ #---v settings
173
+
174
+ RUN ?= #~~ command the tools are run through, such as `docker compose exec <service>`
175
+ _RUN = $(if $(wildcard /.dockerenv),,$(RUN))
176
+
177
+ WORKDIR ?= $(if $(_RUN),/app,$(CURDIR))#~~ where the tools see the sources, which differs from `CURDIR` in a container
178
+ HOST_UID ?= $(or $(shell id -u 2>/dev/null),1000)#vv #~~ user a container writes the bind mount as
179
+ HOST_GID ?= $(or $(shell id -g 2>/dev/null),1000)#vv #~~ group a container writes the bind mount as
180
+
181
+ TARGET_PREFIX ?= #~~ namespaces every target this file defines, so `brnshkr` gives `brnshkr-lint`
182
+ _TARGET_PREFIX = $(if $(TARGET_PREFIX),$(TARGET_PREFIX)$(if $(filter %- %.,$(TARGET_PREFIX)),,-))
183
+
184
+ COLLISION_PREFIX ?= brnshkr#~~ namespace given to a shared target whose name this project already uses
185
+ _COLLISION_PREFIX = $(if $(COLLISION_PREFIX),$(COLLISION_PREFIX)$(if $(filter %- %.,$(COLLISION_PREFIX)),,-))
186
+
187
+ define _MANIFEST_NAME_AWK
188
+ /^[ \t]*[{]?[ \t]*"name"[ \t]*:/ {
189
+ match($$0, /^[ \t]*/)
190
+ if (found_name == "" || RLENGTH < indent) { \
191
+ indent = RLENGTH; split($$0, parts, "\""); \
192
+ found_name = parts[4] \
193
+ }
194
+ }
195
+ END { print found_name }
196
+ endef
197
+
198
+ ifneq ($(filter undefined,$(origin VENDOR) $(origin PACKAGE)),) # manifest
199
+ _MANIFEST := $(firstword $(wildcard $(CURDIR)/package.json $(CURDIR)/composer.json))
200
+ _MANIFEST_NAME := $(if $(_MANIFEST),$(shell $(AWK) '$(_MANIFEST_NAME_AWK)' $(_MANIFEST)))
201
+ endif # /manifest
202
+
203
+ VENDOR ?= $(if $(findstring /,$(_MANIFEST_NAME)),$(firstword $(subst /, ,$(_MANIFEST_NAME))))#~~ vendor name, taken from the manifest when unset
204
+ PACKAGE ?= $(or $(lastword $(subst /, ,$(_MANIFEST_NAME))),$(notdir $(CURDIR)))#~~ package name, taken from the manifest or the directory when unset
205
+ VERSION ?= 0.0.0-dev#vv #~~ package version
206
+ _LABEL = $(VENDOR)$(if $(and $(VENDOR),$(PACKAGE)),/)$(PACKAGE)
207
+
208
+ DEBUG ?= #~~ whether the commands are echoed before they run
209
+ unexport DEBUG # NOTICE: the `debug` package reads this from the environment
210
+ DEBUG_PREFIX ?= $(_ANNOUNCE)$(if $(or $(_IS_DEBUG),$(_IS_TRACE)),,@)#vvv #~~ prefix for a command
211
+ _IS_DEBUG = $(filter-out $(_FALSE),$(DEBUG))
212
+
213
+ TRACE ?= #~~ whether the guards are echoed too, which `DEBUG` leaves out
214
+ TRACE_PREFIX ?= $(if $(_IS_TRACE),,@)#vvv #~~ prefix for a guard command
215
+ _IS_TRACE = $(filter-out $(_FALSE),$(TRACE))
216
+
217
+ ANNOUNCEMENT ?= Running `%s`#~~ message a verb prints before each target it runs, `%s` being the target, empty for none
218
+ _ANNOUNCE = $(if $(and $(_IS_ANNOUNCING),$(ANNOUNCEMENT),$(filter-out _% %-group $(_ANNOUNCED),$@)),$(eval _ANNOUNCED += $@)$(info $(shell $(call log,$(ANNOUNCEMENT),$(COLOR_NOTICE),'$@'))))
219
+
220
+ _FALSE := 0 false off no
221
+ _TRUE := 1 true on yes
222
+ _IS_CI = $(filter-out $(_FALSE),$(CI))
223
+
224
+ _MAKE_FLAGS = $(if $(or $(_IS_DEBUG),$(_IS_TRACE)),--no-print-directory,-s) _IS_ANNOUNCING=
225
+ _VERB_FLAGS = $(_MAKE_FLAGS) $(_INHERITED_FLAGS) $(if $(_IS_PARALLEL),--output-sync=recurse) --keep-going _IS_ANNOUNCING=1
226
+ _IS_PARALLEL = $(filter-out -j1,$(filter -j%,$(MAKEFLAGS)))
227
+
228
+ _UNEXPORTED_VARIABLES = COMPOSER DEBUG ESLINT_FLAGS VITEST
229
+
230
+ _INHERITED_FLAGS = $(foreach VARIABLE,$(_UNEXPORTED_VARIABLES), \
231
+ $(if $(filter environment,$(origin $(VARIABLE))),$(VARIABLE)='$($(VARIABLE))'))
232
+
233
+ BRNSHKR_CONFIG_ERROR_CODE ?= 69#vvv #~~ the error code for custom errors thrown by this Makefile
234
+
235
+ #---vv theming
236
+
237
+ INDENT ?= 1#~~ indent used in `make help` output
238
+ VALUE_WIDTH ?= 48#~~ widest a value column gets in `make help`, past which a value is truncated
239
+
240
+ NO_ANSI ?= #~~ whether ANSI escapes are suppressed
241
+ _IS_NO_ANSI = $(filter-out $(_FALSE),$(NO_ANSI))
242
+ _NO_ANSI_OPTION = $(if $(_IS_NO_ANSI),--no-ansi)
243
+
244
+ THEME ?= #~~ the theme to use
245
+ THEME_BRNSHKR := brnshkr
246
+ THEME_SYMFONY := symfony
247
+
248
+ _THEMES := $(THEME_BRNSHKR) \
249
+ $(THEME_SYMFONY)
250
+
251
+ _THEME_DEFAULT := $(THEME_BRNSHKR)
252
+ _THEME := $(if $(filter $(THEME),$(_THEMES)),$(THEME),$(_THEME_DEFAULT))
253
+
254
+ EDITOR_URL ?= #~~ URL template the entries link to, in `{cwd}`, `{file}`, `{line}` and `{wslDistro}`
255
+ EDITOR_VSCODE := vscode
256
+ EDITOR_PHPSTORM := phpstorm
257
+
258
+ _EDITORS := $(EDITOR_VSCODE) \
259
+ $(EDITOR_PHPSTORM)
260
+
261
+ _EDITOR_URL_VSCODE := vscode://file/{file}:{line}
262
+ _EDITOR_URL_VSCODE_WSL := vscode://vscode-remote/wsl+{wslDistro}{file}:{line}
263
+ _EDITOR_URL_PHPSTORM := phpstorm://open?file={file}&line={line}
264
+ _DETECTED_EDITOR = $(or $(call _editor_from_env),$(call _editor_from_command))
265
+
266
+ EDITOR ?= $(_DETECTED_EDITOR)#~~ the editor to use, empty for no links
267
+ _EDITOR = $(or $(filter $(EDITOR),$(_EDITORS)),$(if $(strip $(EDITOR)),$(if $(findstring environment,$(origin EDITOR)),$(_DETECTED_EDITOR))))
268
+ _EDITOR_UNKNOWN = $(if $(_EDITOR)$(findstring environment,$(origin EDITOR)),,$(strip $(EDITOR)))
269
+
270
+ _EDITOR_URL = $(subst {wslDistro},$(WSL_DISTRO_NAME),$(subst {cwd},$(CURDIR),$(or \
271
+ $(EDITOR_URL), \
272
+ $(if $(filter $(EDITOR_VSCODE),$(_EDITOR)),$(if $(WSL_DISTRO_NAME),$(_EDITOR_URL_VSCODE_WSL),$(_EDITOR_URL_VSCODE))), \
273
+ $(if $(filter $(EDITOR_PHPSTORM),$(_EDITOR)),$(_EDITOR_URL_PHPSTORM)) \
274
+ )))
275
+
276
+ _INLINE_CODE_URL = $(if $(_IS_NO_ANSI),,$(_EDITOR_URL))
277
+
278
+ ifeq ($(origin LOGO),undefined) # logo
279
+ define LOGO
280
+ ___ __ __
281
+ / _ )_______ ____/ / / /__ ____
282
+ / _ / __/ _ \\(_--/ _ \\/ ´_// __/
283
+ /____/_/ /_//_/___/_//_/_/\\_\\/_/
284
+
285
+ endef
286
+ endif # /logo
287
+
288
+ #---- colors
289
+
290
+ COLOR_NORMAL := normal
291
+ COLOR_BLACK := black
292
+ COLOR_RED := red
293
+ COLOR_GREEN := green
294
+ COLOR_YELLOW := yellow
295
+ COLOR_BLUE := blue
296
+ COLOR_MAGENTA := magenta
297
+ COLOR_CYAN := cyan
298
+ COLOR_WHITE := white
299
+
300
+ ifeq ($(_THEME),$(THEME_BRNSHKR)) # theme
301
+ COLOR_PRIMARY ?= $(COLOR_CYAN)
302
+ COLOR_SECONDARY ?= $(COLOR_MAGENTA)
303
+ COLOR_TERTIARY ?= $(COLOR_BLACK) $(MODIFIER_BRIGHT)
304
+ COLOR_ERROR ?= $(COLOR_RED)
305
+ COLOR_WARNING ?= $(COLOR_YELLOW)
306
+ COLOR_NOTICE ?= $(COLOR_MAGENTA)
307
+ COLOR_SUCCESS ?= $(COLOR_GREEN)
308
+ COLOR_LOGO ?= $(COLOR_SECONDARY)
309
+ COLOR_TITLE ?= $(COLOR_PRIMARY) $(MODIFIER_UNDERLINE)
310
+ COLOR_USAGE_ARGUMENTS ?= $(COLOR_PRIMARY)
311
+ COLOR_USAGE_PROGRAM ?= $(COLOR_PRIMARY)
312
+ COLOR_SECTION_LABEL ?= $(COLOR_SECONDARY)
313
+ COLOR_SCOPE ?= $(COLOR_SECONDARY)
314
+ COLOR_ENTRY ?= $(COLOR_PRIMARY)
315
+ COLOR_DESCRIPTION ?= $(COLOR_TERTIARY)
316
+ COLOR_INLINE_CODE ?= $(COLOR_SECONDARY)
317
+ COLOR_HIGHLIGHT ?= $(COLOR_YELLOW)
318
+ else ifeq ($(_THEME),$(THEME_SYMFONY))
319
+ COLOR_PRIMARY ?= $(COLOR_GREEN)
320
+ COLOR_SECONDARY ?= $(COLOR_YELLOW)
321
+ COLOR_TERTIARY ?= $(COLOR_NORMAL)
322
+ COLOR_ERROR ?= $(COLOR_RED)
323
+ COLOR_WARNING ?= $(COLOR_YELLOW)
324
+ COLOR_NOTICE ?= $(COLOR_CYAN)
325
+ COLOR_SUCCESS ?= $(COLOR_GREEN)
326
+ COLOR_LOGO ?= $(COLOR_SECONDARY)
327
+ COLOR_TITLE ?= $(COLOR_PRIMARY) $(MODIFIER_UNDERLINE)
328
+ COLOR_USAGE_ARGUMENTS ?= $(COLOR_TERTIARY)
329
+ COLOR_USAGE_PROGRAM ?= $(COLOR_TERTIARY)
330
+ COLOR_SECTION_LABEL ?= $(COLOR_SECONDARY)
331
+ COLOR_SCOPE ?= $(COLOR_SECONDARY)
332
+ COLOR_ENTRY ?= $(COLOR_PRIMARY)
333
+ COLOR_DESCRIPTION ?= $(COLOR_TERTIARY)
334
+ COLOR_INLINE_CODE ?= $(COLOR_SECONDARY)
335
+ COLOR_HIGHLIGHT ?= $(COLOR_YELLOW)
336
+ endif # /theme
337
+
338
+ _COLORS := $(COLOR_NORMAL) \
339
+ $(COLOR_WHITE) \
340
+ $(COLOR_BLACK) \
341
+ $(COLOR_RED) \
342
+ $(COLOR_YELLOW) \
343
+ $(COLOR_GREEN) \
344
+ $(COLOR_CYAN) \
345
+ $(COLOR_BLUE) \
346
+ $(COLOR_MAGENTA)
347
+
348
+ #----vvv modifiers
349
+
350
+ MODIFIER_NORMAL := normal
351
+ MODIFIER_UNDERLINE := underline
352
+ MODIFIER_BRIGHT := bright
353
+ MODIFIER_BOLD := bold
354
+ MODIFIER_REVERSE := reverse
355
+
356
+ #---vv arguments
357
+
358
+ _TARGET_SOURCES := $(shell $(AWK) -v _PREFIX='$(_TARGET_PREFIX)' -v _CURDIR='$(CURDIR)' \
359
+ 'function does_path_exist(path, line, does_exist) { does_exist = (getline line < path) >= 0; close(path); return does_exist } \
360
+ /^ifneq/ && index($$0, "wildcard") { \
361
+ depth += 1; \
362
+ if (!skip) { \
363
+ count = split(substr($$0, index($$0, "wildcard") + 9), paths, /[[:space:]]+/); \
364
+ has_existing_path = 0; \
365
+ for (i = 1; i <= count; i += 1) { \
366
+ path = paths[i]; \
367
+ if (!index(path, "/")) continue; \
368
+ path = substr(path, index(path, "/")); \
369
+ sub(/[^A-Za-z0-9_.\/-]+$$/, "", path); \
370
+ if (does_path_exist(_CURDIR path)) has_existing_path = 1; \
371
+ } \
372
+ if (!has_existing_path) skip = depth \
373
+ } \
374
+ next \
375
+ } \
376
+ /^ifdef|^ifndef|^ifeq|^ifneq/ { depth += 1; next } \
377
+ /^endif/ { if (skip == depth) skip = 0; depth -= 1; next } \
378
+ skip { next } \
379
+ { while (match($$0, /\$$\(call _target,[a-zA-Z0-9_.-]+\)/)) \
380
+ $$0 = substr($$0, 1, RSTART - 1) "@" substr($$0, RSTART + 15, RLENGTH - 16) substr($$0, RSTART + RLENGTH) } \
381
+ { gsub(/\$$\(_TARGET_PREFIX\)/, _PREFIX) } \
382
+ /^@?[a-zA-Z0-9_.-]+([[:space:]]+@?[a-zA-Z0-9_.-]+)*:([^=]|$$)/ { \
383
+ count = split(substr($$0, 1, index($$0, ":") - 1), ns, /[[:space:]]+/); \
384
+ for (i = 1; i <= count; i += 1) \
385
+ if (ns[i] != "" && ns[i] !~ /^[.]/) printf "%s|%s:%d ", ns[i], FILENAME, FNR \
386
+ }' \
387
+ $(MAKEFILE_LIST) \
388
+ )
389
+
390
+ _TARGET_FILES := $(foreach SOURCE,$(_TARGET_SOURCES),$(firstword $(subst :, ,$(SOURCE))))
391
+ _TARGET_LINKS := $(subst |,=,$(patsubst @%,%,$(_TARGET_SOURCES)))
392
+
393
+ ifeq ($(_TARGET_SOURCES),) # awk
394
+ $(error [$(_LABEL)] `$(AWK)` found no targets, so it is missing or is not an awk)
395
+ endif # /awk
396
+
397
+ #**
398
+ #* The targets a set of files defines.
399
+ #*
400
+ #* parameters:
401
+ #* files: list<string>
402
+ #*
403
+ #* returns: list<string>
404
+ #*
405
+ define _targets_from
406
+ $(sort $(foreach FILE,$1,$(patsubst %|$(FILE),%,$(filter %|$(FILE),$(_TARGET_FILES)))))
407
+ endef
408
+
409
+ #**
410
+ #* Where a target is defined, each as `<file>:<line>`.
411
+ #*
412
+ #* parameters:
413
+ #* target: string
414
+ #*
415
+ #* returns: list<string>
416
+ #*
417
+ define _definitions_of
418
+ $(sort $(patsubst $1|%,%,$(filter $1|%,$(_TARGET_SOURCES))) $(patsubst @$1|%,%,$(filter @$1|%,$(_TARGET_SOURCES))))
419
+ endef
420
+
421
+ #**
422
+ #* The files that define a target, which is what a collision has to name.
423
+ #*
424
+ #* parameters:
425
+ #* target: string
426
+ #*
427
+ #* returns: list<string>
428
+ #*
429
+ define _files_defining
430
+ $(sort $(foreach DEFINITION,$(call _definitions_of,$1),$(firstword $(subst :, ,$(DEFINITION)))))
431
+ endef
432
+
433
+ #**
434
+ #* The name a shared target ends up with.
435
+ #* Its own, unless this project took that name, in which case the first free one behind
436
+ #* `COLLISION_PREFIX`, numbered if the project took that too.
437
+ #*
438
+ #* parameters:
439
+ #* name: string
440
+ #*
441
+ #* returns: string
442
+ #*
443
+ define _target
444
+ $(or $(firstword $(filter-out $(_CONSUMER_TARGETS),$(_TARGET_PREFIX)$1 $(_COLLISION_PREFIX)$(_TARGET_PREFIX)$1 $(foreach \
445
+ NUMBER,1 2 3 4 5 6 7 8 9,$(_COLLISION_PREFIX)$(_TARGET_PREFIX)$1-$(NUMBER)))),$(_COLLISION_PREFIX)$(_TARGET_PREFIX)$1)
446
+ endef
447
+
448
+ _CONSUMER_TARGETS := $(call _targets_from,$(_CONSUMER_MAKEFILES))
449
+ _SCRAPED_SHARED := $(call _targets_from,$(filter-out $(_CONSUMER_MAKEFILES),$(MAKEFILE_LIST)))
450
+ _SHARED_NAMES := $(patsubst @%,%,$(filter @%,$(_SCRAPED_SHARED)))
451
+ _COLLIDING_TARGETS := $(strip $(foreach NAME,$(_SHARED_NAMES),$(if $(filter $(_TARGET_PREFIX)$(NAME),$(_CONSUMER_TARGETS)),$(NAME))))
452
+ _TARGET_NAMES := $(foreach NAME,$(_SHARED_NAMES),$(NAME)=$(call _target,$(NAME)))
453
+ _SHARED_TARGETS := $(sort $(filter-out @%,$(_SCRAPED_SHARED)) $(foreach NAME,$(_SHARED_NAMES),$(call _target,$(NAME))))
454
+ _ALL_TARGETS := $(sort $(_CONSUMER_TARGETS) $(_SHARED_TARGETS))
455
+ _DUPLICATE_TARGETS := $(strip $(foreach NAME,$(_CONSUMER_TARGETS),$(if $(word 2,$(filter $(_CONSUMER_MAKEFILES),$(call _files_defining,$(NAME)))),$(NAME))))
456
+ .DEFAULT_GOAL := $(or $(_DEFAULT_GOAL),$(call _target,help))
457
+
458
+ TARGET := $(firstword $(filter $(_ALL_TARGETS),$(MAKECMDGOALS)))#~~ the first target of this run
459
+ ARG1 = $(word 1,$(ARGS))#~~ the running target's first argument, and so on through `ARG9`
460
+ ARG2 = $(word 2,$(ARGS))
461
+ ARG3 = $(word 3,$(ARGS))
462
+ ARG4 = $(word 4,$(ARGS))
463
+ ARG5 = $(word 5,$(ARGS))
464
+ ARG6 = $(word 6,$(ARGS))
465
+ ARG7 = $(word 7,$(ARGS))
466
+ ARG8 = $(word 8,$(ARGS))
467
+ ARG9 = $(word 9,$(ARGS))
468
+ ARGS = $(call _shell_quoted,$(_ARGS))#~~ every argument the running target was handed, quoted for the shell
469
+
470
+ _ARGS = $(if $(filter command line,$(origin ARGS)),$(ARGS),$(strip $(filter-out $(_ALL_TARGETS),$(call _collect_args,$@,$(MAKECMDGOALS)))))
471
+ _FIXER_ARGS = $(call _shell_quoted,$(call _without_dry_run,$(_ARGS)))
472
+
473
+ #**
474
+ #* The goal a name reaches, empty where it names nothing.
475
+ #*
476
+ #* parameters:
477
+ #* name: string
478
+ #*
479
+ #* returns: string
480
+ #*
481
+ define _goal_of
482
+ $(or $(filter $1,$(_ALL_TARGETS)),$(filter $(call _target,$1),$(_ALL_TARGETS)))
483
+ endef
484
+
485
+ TARGET_ALIASES ?= h=help \
486
+ c=check \
487
+ t=test \
488
+ f=fix#~~ short names for targets, as `<alias>=<target>` pairs
489
+
490
+ _TARGET_ALIAS_MAP = $(foreach PAIR,$(TARGET_ALIASES),$(lastword $(subst =, ,$(PAIR)))=$(firstword $(subst =, ,$(PAIR))))
491
+
492
+ _ALIAS_TARGET = $(strip $(foreach PAIR,$(TARGET_ALIASES), \
493
+ $(if $(filter $(patsubst -%,%,$@),$(firstword $(subst =, ,$(PAIR)))),$(lastword $(subst =, ,$(PAIR))))))
494
+
495
+ _ALIAS_GOAL = $(call _goal_of,$(_ALIAS_TARGET))
496
+
497
+ _TAKEN_ALIASES := $(strip $(foreach PAIR,$(TARGET_ALIASES), \
498
+ $(filter $(firstword $(subst =, ,$(PAIR))),$(_ALL_TARGETS))))
499
+
500
+ _MALFORMED_ALIASES := $(strip $(foreach PAIR,$(TARGET_ALIASES), \
501
+ $(if $(filter 2,$(words $(subst =, ,$(PAIR)))),,$(PAIR))))
502
+
503
+ _DANGLING_ALIASES := $(strip $(foreach PAIR,$(filter-out $(_MALFORMED_ALIASES),$(TARGET_ALIASES)), \
504
+ $(if $(call _goal_of,$(lastword $(subst =, ,$(PAIR)))),,$(PAIR))))
505
+
506
+ ifneq ($(_TAKEN_ALIASES),) # taken
507
+ $(error [$(_LABEL)] `$(firstword $(_TAKEN_ALIASES))` is a target, so it cannot also be an alias)
508
+ endif # /taken
509
+
510
+ ifneq ($(_MALFORMED_ALIASES),) # malformed
511
+ $(error [$(_LABEL)] `$(firstword $(_MALFORMED_ALIASES))` is not an `<alias>=<target>` pair)
512
+ endif # /malformed
513
+
514
+ ifneq ($(_DANGLING_ALIASES),) # dangling
515
+ $(error [$(_LABEL)] `$(subst =,` aliases `,$(firstword $(_DANGLING_ALIASES)))`, which is not a target)
516
+ endif # /dangling
517
+
518
+ _IS_JUST_PRINT = $(findstring n,$(firstword $(MAKEFLAGS)))
519
+ _IS_FIRST_GOAL = $(filter $@,$(firstword $(MAKECMDGOALS)))
520
+ _UNKNOWN_ARGS = $(call _shell_quoted,$(filter-out $@,$(_ARGS)))
521
+ _SUGGESTED_FOR = $@
522
+ _TARGET_GOALS = $(filter-out _% -%,$(_ALL_TARGETS))
523
+ _STAGE_GOALS = $(foreach STAGE,$(_DOTENV_STAGES),$(addprefix $(STAGE)-,$(_TARGET_GOALS)))
524
+
525
+ #**
526
+ #* The goals tied for nearest to what was typed, at most three.
527
+ #*
528
+ #* parameters:
529
+ #* goals: list<string>
530
+ #* typed_name?: string = $(_SUGGESTED_FOR)
531
+ #*
532
+ #* returns: list<string>
533
+ #*
534
+ define _suggestions
535
+ $(shell $(PRINTF) '%s\n' $1 \
536
+ | $(AWK) -v _UNKNOWN='$(or $2,$(_SUGGESTED_FOR))' ' \
537
+ function levenshtein(left, right, left_length, right_length, left_index, right_index, cost, best, previous, current) { \
538
+ left_length = length(left); \
539
+ right_length = length(right); \
540
+ for (right_index = 0; right_index <= right_length; right_index += 1) previous[right_index] = right_index; \
541
+ for (left_index = 1; left_index <= left_length; left_index += 1) { \
542
+ current[0] = left_index; \
543
+ for (right_index = 1; right_index <= right_length; right_index += 1) { \
544
+ cost = substr(left, left_index, 1) == substr(right, right_index, 1) ? 0 : 1; \
545
+ best = previous[right_index] + 1; \
546
+ if (current[right_index - 1] + 1 < best) best = current[right_index - 1] + 1; \
547
+ if (previous[right_index - 1] + cost < best) best = previous[right_index - 1] + cost; \
548
+ current[right_index] = best \
549
+ } \
550
+ for (right_index = 0; right_index <= right_length; right_index += 1) previous[right_index] = current[right_index]; \
551
+ } \
552
+ return previous[right_length] \
553
+ } \
554
+ { measured = levenshtein(_UNKNOWN, $$0); if (measured <= (length(_UNKNOWN) > 4 ? 2 : 1) || (length(_UNKNOWN) >= 3 && index($$0, _UNKNOWN))) printf "%d %s\n", measured, $$0 }' \
555
+ | sort -n \
556
+ | $(AWK) 'NR == 1 { nearest = $$1 } $$1 == nearest && NR <= 3 { print $$2 }')
557
+ endef
558
+
559
+ _SUGGESTED_TARGETS = $(if $(filter -%,$(_SUGGESTED_FOR)),,$(or \
560
+ $(call _suggestions,$(_TARGET_GOALS)),$(call _suggestions,$(_STAGE_GOALS))))
561
+
562
+ _ARG_VALUES =
563
+ _UNKNOWN_VALUES = $(if $(_ARG_VALUES),$(filter-out $(_ARG_VALUES),$(_ARG_WORDS)))
564
+ _MISTYPED_VALUE = $(firstword $(_UNKNOWN_VALUES))
565
+ _SUGGESTED_VALUES = $(call _suggestions,$(_ARG_VALUES),$(_MISTYPED_VALUE))
566
+
567
+ #--- common
568
+
569
+ _COMMA := ,
570
+ _QUOTE := '
571
+ _SPACE := $(subst ,, )
572
+
573
+ define _NEWLINE
574
+
575
+
576
+ endef
577
+
578
+ _PACKAGE_CONF_DIRS = $(WORKDIR)/vendor/brnshkr/config/conf \
579
+ $(WORKDIR)/node_modules/@brnshkr/config/conf
580
+
581
+ _AUTOLOADER = $(WORKDIR)/vendor/autoload.php
582
+ _TRACKED_EXTENSIONS := $(sort $(suffix $(shell $(GIT) ls-files 2>/dev/null)))
583
+ _FIXER_NAMES :=
584
+ _ANALYZER_NAMES :=
585
+ _GROUP_NAMES :=
586
+ _TEST_NAMES :=
587
+ _COVERAGE_NAMES :=
588
+ _PACK_NAMES :=
589
+ _PLAIN_CONFIGS = $(WORKDIR)/.gitignore
590
+ _LAYERED_CONFIGS =
591
+ _LINKED_CONFIGS =
592
+
593
+ _PLACED_CONFIGS = $(WORKDIR)/.editorconfig|$(_BRNSHKR_CONFIG_DIR)/editorconfig.dist \
594
+ $(WORKDIR)/.vscode/settings.json|$(_BRNSHKR_CONFIG_DIR)/vscode-settings.dist.jsonc \
595
+ $(WORKDIR)/.vscode/extensions.json|$(_BRNSHKR_CONFIG_DIR)/vscode-extensions.dist.json \
596
+ $(WORKDIR)/.vscode/css-custom-data.json|$(_BRNSHKR_CONFIG_DIR)/vscode-css-custom-data.dist.json \
597
+ $(if $(wildcard $(CURDIR)/package.json),$(WORKDIR)/bunfig.toml|$(_BRNSHKR_CONFIG_DIR)/bunfig.dist.toml) \
598
+ $(if $(wildcard $(CURDIR)/composer.json),$(WORKDIR)/.gitattributes|$(_BRNSHKR_CONFIG_DIR)/gitattributes.dist)
599
+
600
+ $(call _target,help): #~~ show this help, narrowed by any scope named and widened by `v`, `vv` or `vvv`
601
+ $(DEBUG_PREFIX)$(call _make_vars_as_env) \
602
+ _CURDIR="$(abspath $(CURDIR))" \
603
+ _WORKDIR="$(WORKDIR)" \
604
+ _TARGET_PREFIX="$(_TARGET_PREFIX)" \
605
+ _HEADER_LOGO="$(if $(LOGO),$$($(PRINTF) '%b' '$(call _color,$(COLOR_LOGO))$(subst $(_NEWLINE),\n,$(LOGO))$(_ANSI_RESET)__BRNSHKR_LOGO_END__'),)" \
606
+ _HEADER_TITLE="$(if $(or $(PACKAGE),$(VERSION)),$$($(PRINTF) '%b' '$(call text,$(if $(PACKAGE),$(call _to_title_case,$(PACKAGE)))$(if $(and $(PACKAGE),$(VERSION)), )$(if $(VERSION),($(strip $(VERSION)))),$(COLOR_TITLE))'),)" \
607
+ _HEADER_USAGE="$$($(PRINTF) '%b' '$(call text,Usage:,$(COLOR_SECTION_LABEL))\n $(call text,make,$(COLOR_USAGE_PROGRAM)) $(call text,<command> [--] [arguments],$(COLOR_USAGE_ARGUMENTS))')" \
608
+ _SCOPES="$(subst .,/,$(_HELP_SCOPES))" \
609
+ _HAS_LIST_SCOPES_ARGUMENT="$(_HAS_LIST_SCOPES_ARGUMENT)" \
610
+ _HAS_RESOLVE_ARGUMENT="$(_HAS_RESOLVE_ARGUMENT)" \
611
+ _INLINE_CODE_OPEN="$$($(PRINTF) '%b' '$(call _ansi,$(COLOR_INLINE_CODE))')" \
612
+ _INLINE_CODE_CLOSE="$$($(PRINTF) '%b' '$(_ANSI_RESET)$(call _ansi,$(COLOR_DESCRIPTION))')" \
613
+ _VERBOSITY="$(or $(V),$(_HELP_VERBOSITY))" \
614
+ LC_ALL=C \
615
+ $(AWK) "$$_HELP_AWK" \
616
+ $(MAKEFILE_LIST)
617
+
618
+ STARTUP_TARGETS ?= #v #~~ this project's own targets, run by `startup` after the dependencies
619
+
620
+ _STARTUP := _$(_TARGET_PREFIX)install-missing-hooks \
621
+ $(if $(_IS_INSTALLING),,$(if $(wildcard $(CURDIR)/composer.json),_$(_TARGET_PREFIX)composer-install) \
622
+ $(if $(wildcard $(CURDIR)/package.json),_$(_TARGET_PREFIX)bun-install))
623
+
624
+ $(call _target,startup): $(_STARTUP) #~~ brings a fresh checkout to a workable state #v
625
+ $(DEBUG_PREFIX)$(MAKE) $(_VERB_FLAGS) --no-keep-going $(call _target,configs)
626
+ $(DEBUG_PREFIX)$(if $(STARTUP_TARGETS),$(MAKE) $(_VERB_FLAGS) --no-keep-going $(STARTUP_TARGETS),:)
627
+
628
+ GIT_HOOKS ?= #v #~~ git hooks this project installs, as `<hook>=<target>` pairs
629
+
630
+ $(call _target,install-hooks): #~~ installs this project's git hooks, replacing any that exist #v
631
+ $(TRACE_PREFIX)$(GIT) rev-parse --is-inside-work-tree >/dev/null 2>&1 || { \
632
+ $(call log,`install-hooks` needs a git checkout.,$(COLOR_ERROR)) >&2; \
633
+ exit $(BRNSHKR_CONFIG_ERROR_CODE); \
634
+ }
635
+ $(DEBUG_PREFIX)$(call _install_git_hooks,yes)
636
+
637
+ _$(_TARGET_PREFIX)install-missing-hooks:
638
+ $(DEBUG_PREFIX)$(call _install_git_hooks)
639
+
640
+ CONFIG ?= #v #~~ `local` or `dist` to pin which config every tool reads, rather than the first that is there
641
+ ARCHIVE_EXTRA_PATHS ?= #v #~~ paths an archive ships beside the autoload roots, a trailing `/` taking the whole tree
642
+
643
+ _PINNED_CONFIG = $(filter local dist,$(CONFIG))
644
+ _TEMPLATE_DIRS = $(_BRNSHKR_CONFIG_DIR) $(filter-out $(_BRNSHKR_CONFIG_DIR),$(wildcard $(CURDIR)/vendor/brnshkr/config/conf $(CURDIR)/node_modules/@brnshkr/config/conf))
645
+
646
+ _NAMED_CONFIGS = $(filter-out local l force f,$(_ARG_WORDS))
647
+ _IS_FORCED_CONFIG = $(filter force f,$(_ARG_WORDS))
648
+ _CONFIG_NAMES = $(sort $(foreach CONFIG,$(_CONFIGS) $(_PLACED_CONFIGS) $(_LINKED_CONFIGS),$(call _config_name,$(CONFIG))))
649
+
650
+ _CONFIGS = $(call _in_project,$(_PLAIN_CONFIGS) $(foreach CONFIG,$(_LAYERED_CONFIGS),$(call _tracked,$(CONFIG))) \
651
+ $(if $(filter local l,$(_ARG_WORDS)),$(subst .dist.,.,$(_LAYERED_CONFIGS))))
652
+
653
+ $(call _target,configs): #~~ writes the tool configs this project is missing, `local` its private ones too #v
654
+ $(TRACE_PREFIX)$(call _require_known_value,config)
655
+ $(DEBUG_PREFIX)for target in $(call _host_path,$(call _selected_configs,$(_CONFIGS))); do \
656
+ if [ -f "$$target" ]; then \
657
+ $(if $(_IS_FORCED_CONFIG),:,$(call log,`%s` already exists.,$(COLOR_NOTICE),"./$${target#$(CURDIR)/}")); \
658
+ $(if $(_IS_FORCED_CONFIG),:,$(if $(_NAMED_CONFIGS),$(call confirm,Overwrite it?),false)) || continue; \
659
+ fi; \
660
+ name="$${target##*/}"; \
661
+ source="$${target}.example"; \
662
+ case "$$name" in \
663
+ *.dist.*) shipped="$$name"; tracked="";; \
664
+ *.mjs|*.php) shipped=""; tracked="";; \
665
+ ?*.?*) shipped=""; tracked="$${name%.*}.dist.$${name##*.}";; \
666
+ *) shipped=""; tracked="$${name}.dist";; \
667
+ esac; \
668
+ for directory in $(_TEMPLATE_DIRS); do \
669
+ [ -f "$$source" ] && break; \
670
+ for candidate in "$$shipped" "$$name.example" "$$tracked"; do \
671
+ [ -n "$$candidate" ] || continue; \
672
+ source="$$directory/$$candidate"; \
673
+ [ -f "$$source" ] && break; \
674
+ done; \
675
+ done; \
676
+ case "$$target" in */*) $(MKDIR) -p "$${target%/*}";; esac; \
677
+ if [ -f "$$source" ]; then \
678
+ $(CP) "$$source" "$$target" || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
679
+ else \
680
+ extended="$${target%/*}/$${name%.*}.dist.$${name##*.}"; \
681
+ case "$$name" in \
682
+ *.mjs) $(PRINTF) '%s\n' "export { default } from './$${extended##*/}';" > "$$target" \
683
+ || exit $(BRNSHKR_CONFIG_ERROR_CODE);; \
684
+ *.php) $(PRINTF) '%s\n' '<?php' '' 'declare(strict_types=1);' '' '/**' ' * @internal' ' */' \
685
+ "\$$config = include __DIR__ . '/$${extended##*/}';" '' 'return $$config;' > "$$target" \
686
+ || exit $(BRNSHKR_CONFIG_ERROR_CODE);; \
687
+ *) extended=""; continue;; \
688
+ esac; \
689
+ fi; \
690
+ case "$$target" in $(CURDIR)/*.php) $(call _name_internal_tag,$${target#$(CURDIR)/});; esac; \
691
+ $(call log,Created `%s`.,$(COLOR_SUCCESS),"./$${target#$(CURDIR)/}"); \
692
+ if [ -n "$$extended" ]; then \
693
+ $(call log,Built on `%s` — keep that include.,$(COLOR_NOTICE),"./$${extended#$(CURDIR)/}"); \
694
+ extended=""; \
695
+ fi; \
696
+ done
697
+ $(DEBUG_PREFIX)for entry in $(foreach PLACED_CONFIG,$(call _selected_configs,$(_PLACED_CONFIGS)),'$(PLACED_CONFIG)'); do \
698
+ target="$${entry%%|*}"; \
699
+ source="$${entry#*|}"; \
700
+ [ -f "$$source" ] || continue; \
701
+ if [ -f "$$target" ]; then \
702
+ $(if $(_IS_FORCED_CONFIG),:,$(call log,`%s` already exists.,$(COLOR_NOTICE),"./$${target#$(CURDIR)/}")); \
703
+ $(if $(_IS_FORCED_CONFIG),:,$(if $(_NAMED_CONFIGS),$(call confirm,Overwrite it?),false)) || continue; \
704
+ fi; \
705
+ case "$$target" in */*) $(MKDIR) -p "$${target%/*}";; esac; \
706
+ $(CP) "$$source" "$$target" || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
707
+ $(call log,Created `%s`.,$(COLOR_SUCCESS),"./$${target#$(CURDIR)/}"); \
708
+ done
709
+ $(DEBUG_PREFIX)$(if $(wildcard $(CURDIR)/composer.json),[ ! -f $(WORKDIR)/.gitattributes ] || { \
710
+ updated_path=$$($(PHP) -r "$$_GITATTRIBUTES_PROGRAM" $(WORKDIR)/.gitattributes $(WORKDIR)/composer.json '$(strip $(ARCHIVE_EXTRA_PATHS))') || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
711
+ [ -z "$$updated_path" ] || $(call log,Updated `%s`.,$(COLOR_SUCCESS),'$(call _named_path,$(WORKDIR)/.gitattributes)'); \
712
+ },:)
713
+ $(DEBUG_PREFIX)for entry in $(foreach LINKED_CONFIG,$(call _selected_configs,$(_LINKED_CONFIGS)),'$(LINKED_CONFIG)'); do \
714
+ link="$${entry%%|*}"; \
715
+ source="$${entry#*|}"; \
716
+ relative="$${source#$(CURDIR)/}"; \
717
+ if [ -e "$$link" ]; then \
718
+ $(if $(_IS_FORCED_CONFIG),:,$(call log,`%s` already exists.,$(COLOR_NOTICE),"./$${link#$(CURDIR)/}")); \
719
+ $(if $(_IS_FORCED_CONFIG),:,$(if $(_NAMED_CONFIGS),$(call confirm,Overwrite it?),false)) || continue; \
720
+ fi; \
721
+ [ -f "$$source" ] || continue; \
722
+ if $(LN) -sf "$$relative" "$$link" 2>/dev/null; then \
723
+ $(call log,Linked `%s` to `%s`.,$(COLOR_SUCCESS),"./$${link#$(CURDIR)/}" "./$$relative"); \
724
+ else \
725
+ $(PRINTF) '%s\n' '{' " \"extends\": \"./$$relative\"" '}' > "$$link" \
726
+ || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
727
+ $(call log,Created `%s`.,$(COLOR_SUCCESS),"./$${link#$(CURDIR)/}"); \
728
+ fi; \
729
+ done
730
+
731
+ CACHE_DIR ?= $(CURDIR)/.cache#v #~~ directory the tools keep their caches in
732
+ _CACHES = $(if $(CACHE_DIR),$(notdir $(wildcard $(CACHE_DIR)/*)))
733
+
734
+ $(call _target,cc): #~~ removes the caches named, or all of them when given none #v
735
+ $(TRACE_PREFIX)$(call _require_known_value,cache)
736
+ $(DEBUG_PREFIX)if [ -z "$(_CACHES)" ]; then \
737
+ $(call log,No caches to remove.,$(COLOR_NOTICE)); \
738
+ elif [ -n "$(strip $(ARGS))" ]; then \
739
+ for name in $(sort $(ARGS)); do \
740
+ $(RM) -rf "$(CACHE_DIR)/$$name" || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
741
+ $(call log,Removed `%s/%s`.,$(COLOR_SUCCESS),'$(call _named_path,$(CACHE_DIR))' "$$name"); \
742
+ done; \
743
+ else \
744
+ $(call log,This removes every cache in `%s`.,$(COLOR_WARNING),'$(call _named_path,$(CACHE_DIR))'); \
745
+ $(call log,Name %s to remove only some.,$(COLOR_NOTICE),'$(call _join_as_quoted_list,$(_CACHES),disjunction)',no-autolink); \
746
+ if $(call confirm,Continue?); then \
747
+ $(RM) -rf "$(CACHE_DIR)" || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
748
+ $(call log,Removed `%s`.,$(COLOR_SUCCESS),'$(call _named_path,$(CACHE_DIR))'); \
749
+ else \
750
+ $(call log,Nothing removed.,$(COLOR_NOTICE)); \
751
+ fi; \
752
+ fi
753
+
754
+ _GIT_CLEAN_BASE_FLAGS = $(if $(filter --all -a,$(_ARGS)),,-e /.local -e .env*.local)
755
+ _GIT_CLEAN_DRY_FLAGS = -xdffn $(_GIT_CLEAN_BASE_FLAGS)
756
+ _GIT_CLEAN_FORCE_FLAGS = -xdff $(_GIT_CLEAN_BASE_FLAGS)
757
+
758
+ $(call _target,fresh): #~~ removes every untracked file and runs `startup`, `--all` the private ones too #vv
759
+ $(TRACE_PREFIX)$(GIT) rev-parse --is-inside-work-tree >/dev/null 2>&1 || { \
760
+ $(call log,`fresh` needs a git checkout.,$(COLOR_ERROR)) >&2; \
761
+ exit $(BRNSHKR_CONFIG_ERROR_CODE); \
762
+ }
763
+ $(DEBUG_PREFIX)LC_ALL=C $(GIT) clean $(_GIT_CLEAN_DRY_FLAGS) | while read -r path; do \
764
+ $(call log,Would remove `./%s`.,$(COLOR_NOTICE),"$${path#Would remove }"); \
765
+ done
766
+ $(DEBUG_PREFIX)$(if $(_IS_JUST_PRINT)$(call _is_dry_run,$(_ARGS)),:,$(if $(filter --force -f,$(_ARGS)),:,$(call confirm,Remove them and reinstall?) || { \
767
+ $(call log,Nothing removed.,$(COLOR_NOTICE)); \
768
+ exit 0; \
769
+ }); \
770
+ $(GIT) clean $(_GIT_CLEAN_FORCE_FLAGS))
771
+ $(DEBUG_PREFIX)$(if $(_IS_JUST_PRINT)$(call _is_dry_run,$(_ARGS)),:,[ -n "$$($(GIT) clean $(_GIT_CLEAN_DRY_FLAGS))" ] || { \
772
+ { [ -f $(_BRNSHKR_CONFIG_MAKEFILE) ] \
773
+ || { $(if $(_COMPOSER_INSTALL),$(_COMPOSER_INSTALL) &&) $(if $(_BUN_INSTALL),$(_BUN_INSTALL) &&) is_installing=_IS_INSTALLING=1; }; } \
774
+ && $(MAKE) $(_VERB_FLAGS) --no-keep-going $$is_installing $(call _target,startup); \
775
+ })
776
+
777
+ $(call _target,fresh-dry-run): #~~ lists what `fresh` would remove #vv
778
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,fresh) -- --dry-run $(ARGS)
779
+
780
+ #---vvv semver
781
+
782
+ #---- constants
783
+
784
+ SEMVER_NUMBER ?= (0|[1-9][0-9]*)#~~ a numeric identifier, which carries no leading zero
785
+ SEMVER_PRERELEASE_PART ?= (0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)#~~ one prerelease identifier, which carries no leading zero when it is numeric
786
+ SEMVER_PRERELEASE ?= $(SEMVER_PRERELEASE_PART)(\.$(SEMVER_PRERELEASE_PART))*#~~ the prerelease grammar semver itself permits
787
+ SEMVER_PRERELEASE_STRICT ?= (alpha|beta|rc)(\.(0|[1-9][0-9]*))?#~~ the narrower one: what npm and Composer can both express, spelled npm's way
788
+ SEMVER_BUILD ?= [0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*#~~ build metadata, which neither registry accepts in a published version
789
+ SEMVER_REGEX ?= $(SEMVER_NUMBER)\.$(SEMVER_NUMBER)\.$(SEMVER_NUMBER)(-$(SEMVER_PRERELEASE))?(\+$(SEMVER_BUILD))?#~~ the semantic version grammar, as a POSIX extended regular expression
790
+
791
+ #---v dotenv
792
+
793
+ DOTENV ?= 1#~~ base path of the environment files, `1` for `.env`, or `0` to load none
794
+ DOTENV_ENV_KEY ?= APP_ENV#~~ variable naming the current environment
795
+ DOTENV_DEFAULT_ENV ?= dev#~~ environment assumed when the variable is unset
796
+ DOTENV_TEST_ENVS ?= test#~~ environments that skip the `.local` file, as a test run must be reproducible
797
+ _DOTENV_BASE := $(if $(filter-out $(_FALSE),$(DOTENV)),$(CURDIR)/$(or $(filter-out $(_TRUE),$(DOTENV)),.env))
798
+ _DOTENV_SEPARATOR := @@brnshkr-dotenv-newline@@
799
+ _DOTENV_STAGES := $(filter-out local %.local %.example,$(patsubst .env.%,%,$(notdir $(wildcard $(CURDIR)/.env.*))))
800
+
801
+ _DOTENV_RESET = $(if $(_DOTENV_FILE_KEYS),env $(foreach KEY,$(sort $(_DOTENV_FILE_KEYS)),-u $(KEY)) )
802
+
803
+ define _DOTENV_AWK
804
+ function fail(message) { gsub(/\$$/, "$$$$", message); printf "$$(error [%s] %s)\n", _PREFIX, message; exit 1 }
805
+ function unquote(value, quote) {
806
+ quote = substr(value, 1, 1)
807
+ if ((quote == "\"" || quote == SQ) && length(value) > 1 && substr(value, length(value), 1) == quote) return substr(value, 2, length(value) - 2)
808
+ return value
809
+ }
810
+ function interpolate(value, out, name, start, stop) {
811
+ out = ""
812
+ while ((start = index(value, "$${")) > 0) {
813
+ stop = index(substr(value, start), "}")
814
+ if (stop == 0) break
815
+ name = substr(value, start + 2, stop - 3)
816
+ out = out substr(value, 1, start - 1) ((name in ENVIRON) ? ENVIRON[name] : (name in values) ? values[name] : "")
817
+ value = substr(value, start + stop)
818
+ }
819
+ return out value
820
+ }
821
+ function load(path, line, key, value, quote, continued) {
822
+ if ((getline line < path) < 0) return 0
823
+ do {
824
+ sub(/\r$$/, "", line)
825
+ if (line ~ /^[ \t]*($$|#)/) continue
826
+ sub(/^[ \t]*export[ \t]+/, "", line)
827
+ if (line !~ /^[A-Za-z_][A-Za-z0-9_]*[ \t]*=/) continue
828
+ key = line
829
+ sub(/[ \t]*=.*$$/, "", key)
830
+ value = substr(line, index(line, "=") + 1)
831
+ sub(/^[ \t]+/, "", value)
832
+ quote = substr(value, 1, 1)
833
+ if (quote == "\"" || quote == SQ) {
834
+ while (!(length(value) > 1 && substr(value, length(value), 1) == quote)) {
835
+ if ((getline continued < path) <= 0) fail("unterminated quote in `" path "`")
836
+ sub(/\r$$/, "", continued)
837
+ value = value "\n" continued
838
+ }
839
+ } else {
840
+ sub(/[ \t]+#.*$$/, "", value)
841
+ sub(/[ \t]+$$/, "", value)
842
+ }
843
+ if (quote != SQ && value ~ /\$$\(/) fail("`$$(...)` in `" path "` is not supported, use `$${VAR}`")
844
+ value = unquote(value)
845
+ if (quote != SQ) value = interpolate(value)
846
+ if (!(key in values)) order[++count] = key
847
+ values[key] = value
848
+ } while ((getline line < path) > 0)
849
+ close(path)
850
+ return 1
851
+ }
852
+ function environment(fallback, value) { value = _ENV_VALUE != "" ? _ENV_VALUE : (_ENV_KEY in values) ? values[_ENV_KEY] : ""; return value != "" ? value : fallback }
853
+ BEGIN {
854
+ SQ = sprintf("%c", 39)
855
+ if (!load(_BASE)) load(_BASE ".dist")
856
+ env = environment(_DEFAULT_ENV)
857
+ split(_TEST_ENVS, tests, /[ \t]+/)
858
+ for (i in tests) if (tests[i] == env) is_test = 1
859
+ if (!is_test) { load(_BASE ".local"); env = environment(env) }
860
+ if (env != "local") { load(_BASE "." env); load(_BASE "." env ".local") }
861
+ if (!(_ENV_KEY in values)) order[++count] = _ENV_KEY
862
+ values[_ENV_KEY] = env
863
+ for (i = 1; i <= count; i += 1) {
864
+ key = order[i]
865
+ value = values[key]
866
+ gsub(/\$$/, "$$$$", value)
867
+ gsub(/#/, "\\#", value)
868
+ if (value ~ /\n/) {
869
+ gsub(/\n/, _SEPARATOR, value)
870
+ printf "define %s ?=%s%s%sendef%s", key, _SEPARATOR, value, _SEPARATOR, _SEPARATOR
871
+ } else {
872
+ printf "%s ?= %s%s", key, value, _SEPARATOR
873
+ }
874
+ printf "export %s%s", key, _SEPARATOR
875
+ if (!(key in ENVIRON)) printf "_DOTENV_FILE_KEYS += %s%s", key, _SEPARATOR
876
+ }
877
+ }
878
+ endef
879
+
880
+ _DOTENV_ASSIGNMENTS := $(if $(wildcard $(_DOTENV_BASE) $(_DOTENV_BASE).dist),$(shell \
881
+ $(AWK) \
882
+ -v _BASE='$(_DOTENV_BASE)' \
883
+ -v _ENV_KEY='$(DOTENV_ENV_KEY)' \
884
+ -v _ENV_VALUE='$($(DOTENV_ENV_KEY))' \
885
+ -v _DEFAULT_ENV='$(DOTENV_DEFAULT_ENV)' \
886
+ -v _TEST_ENVS='$(DOTENV_TEST_ENVS)' \
887
+ -v _PREFIX='$(_LABEL)' \
888
+ -v _SEPARATOR='$(_DOTENV_SEPARATOR)' \
889
+ '$(_DOTENV_AWK)' \
890
+ ))
891
+
892
+ $(eval $(subst $(_DOTENV_SEPARATOR),$(_NEWLINE),$(_DOTENV_ASSIGNMENTS)))
893
+
894
+ #**
895
+ #* Defines `<stage>-<target>`, which runs that target with the environment key set to that stage.
896
+ #*
897
+ #* parameters:
898
+ #* stage: string
899
+ #*
900
+ #* returns: void
901
+ #*
902
+ define _stage_rule
903
+ $1-%: _SUGGESTED_FOR = $$*
904
+ $1-%:
905
+ $$(TRACE_PREFIX)$$(if $$(call _goal_of,$$*),:,$$(call log,Unknown command `%s`.,$$(COLOR_ERROR),'$$@'); \
906
+ $$(call _suggest,$1-))
907
+ $$(DEBUG_PREFIX)$$(if $$(call _goal_of,$$*),$$(_DOTENV_RESET)$$(MAKE) $$(_MAKE_FLAGS) $$(_INHERITED_FLAGS) $(DOTENV_ENV_KEY)=$1 $$* \
908
+ $$(if $$(_UNKNOWN_ARGS),-- $$(_UNKNOWN_ARGS)),:)
909
+ endef
910
+
911
+ $(foreach STAGE,$(_DOTENV_STAGES),$(eval $(call _stage_rule,$(STAGE))))
912
+
913
+ #---v helpers
914
+
915
+ #**
916
+ #* The value it is given when this project tracks a file the extensions name, or when git cannot say.
917
+ #*
918
+ #* parameters:
919
+ #* value: list<string>
920
+ #* extensions: list<string>
921
+ #*
922
+ #* returns: list<string>
923
+ #*
924
+ define _when_tracked
925
+ $(if $(_TRACKED_EXTENSIONS),$(if $(filter $(addprefix .,$2),$(_TRACKED_EXTENSIONS)),$1),$1)
926
+ endef
927
+
928
+ #**
929
+ #* The value it is given when the path is there, so a list only claims what this project has.
930
+ #*
931
+ #* parameters:
932
+ #* value: list<string>
933
+ #* path: string
934
+ #*
935
+ #* returns: list<string>
936
+ #*
937
+ define _when_installed
938
+ $(if $(wildcard $(CURDIR)/$2),$1)
939
+ endef
940
+
941
+ ifneq ($(wildcard $(CURDIR)/composer.json),) # php
942
+
943
+ #--- php
944
+
945
+ PHP_EXTENSIONS ?= php#vv #~~ extensions the PHP tools read
946
+
947
+ #--- composer
948
+
949
+ COMPOSER ?= $(_RUN) composer#v #~~ command that runs `composer`
950
+ unexport COMPOSER # NOTICE: Composer reads this as the manifest to open
951
+ COMPOSER_FLAGS ?= $(if $(_IS_CI),--no-interaction --no-progress) $(if $(_IS_DEBUG),-vvv)#v #~~ additional flags passed to `composer`
952
+ _COMPOSER_PACKAGE_DIR := $(CURDIR)/.local/$(if $(VENDOR),$(patsubst @%,%,$(VENDOR))/)$(PACKAGE)
953
+ _COMPOSER_INSTALL = $(COMPOSER) install $(COMPOSER_FLAGS)
954
+
955
+ _PACK_NAMES += composer-pack
956
+
957
+ $(call _target,composer): #~~ runs `composer`
958
+ $(DEBUG_PREFIX)$(COMPOSER) $(COMPOSER_FLAGS) $(ARGS)
959
+
960
+ $(call _target,composer-list): #~~ lists the files the composer package would ship #vv
961
+ $(DEBUG_PREFIX)$(COMPOSER) archive --quiet --format=tar --dir=$(WORKDIR) --file=package \
962
+ && $(TAR) -tf package.tar \
963
+ && $(RM) -f package.tar
964
+
965
+ $(call _target,composer-pack): #~~ packs the composer package into `./.local/<VENDOR>/<PACKAGE>` #vvv
966
+ $(DEBUG_PREFIX)$(RM) -rf $(_COMPOSER_PACKAGE_DIR) \
967
+ && $(MKDIR) -p $(_COMPOSER_PACKAGE_DIR) \
968
+ && $(COMPOSER) archive --quiet --format=tar --dir=$(WORKDIR) --file=package \
969
+ && $(TAR) -xf package.tar -C $(_COMPOSER_PACKAGE_DIR) \
970
+ && $(RM) -f package.tar
971
+
972
+ $(call _target,composer-print): #~~ prints the resolved `composer` configuration #vv
973
+ $(DEBUG_PREFIX)$(COMPOSER) config --list $(COMPOSER_FLAGS) $(ARGS)
974
+
975
+ _$(_TARGET_PREFIX)composer-install:
976
+ $(DEBUG_PREFIX)$(_COMPOSER_INSTALL)
977
+
978
+ #--- phpunit
979
+
980
+ ifneq ($(wildcard $(CURDIR)/vendor/bin/pest $(CURDIR)/vendor/bin/phpunit),) # phpunit
981
+
982
+ PEST ?= $(_RUN) $(_PEST_BINARY)#vvv #~~ command that runs `pest`
983
+ PHP_UNIT ?= $(if $(wildcard $(CURDIR)/vendor/bin/pest),$(PEST),$(_RUN) $(WORKDIR)/vendor/bin/phpunit)#v #~~ command that runs the tests, which is Pest when it is installed
984
+ PHP_UNIT_CONFIG ?= $(call _config,$(WORKDIR)/conf/phpunit.xml $(WORKDIR)/conf/phpunit.dist.xml,php)#v #~~ path to the test runner config
985
+ PHP_UNIT_FLAGS ?= $(if $(_IS_DEBUG),--debug) $(if $(_IS_NO_ANSI),--colors=never)#v #~~ additional flags passed to the test runner
986
+ PHP_UNIT_SNAPSHOT_FLAGS ?= $(if $(_IS_PEST),--update-snapshots) --do-not-fail-on-incomplete#v #~~ flags that make the runner rewrite its snapshots
987
+ PHP_UNIT_COVERAGE_DIR ?= $(WORKDIR)/.cache/phpunit.cache/coverage#v #~~ directory the HTML coverage report is written to
988
+ PHP_UNIT_COVERAGE_FILE ?= $(PHP_UNIT_COVERAGE_DIR)/coverage.txt#vv #~~ text report the minimum is read from, where the runner has no minimum of its own
989
+ PHP_UNIT_MIN_COVERAGE ?= 100#v #~~ percentage every metric must reach, `0` to collect coverage without a floor
990
+ PHP_UNIT_MIN_COVERAGE_CLASSES ?= $(PHP_UNIT_MIN_COVERAGE)#vv #~~ percentage the class metric must reach, which Pest cannot read
991
+ PHP_UNIT_MIN_COVERAGE_METHODS ?= $(PHP_UNIT_MIN_COVERAGE)#vv #~~ percentage the method metric must reach, which Pest cannot read
992
+ PHP_UNIT_MIN_COVERAGE_LINES ?= $(PHP_UNIT_MIN_COVERAGE)#vv #~~ percentage the line metric must reach
993
+ PHP_UNIT_COVERAGE_FLAGS ?= $(if $(_IS_PEST),--coverage $(if $(_PHP_UNIT_FLOOR),--min=$(_PHP_UNIT_FLOOR)),$(if $(_PHP_UNIT_FLOORS),--coverage-text=$(PHP_UNIT_COVERAGE_FILE))) --coverage-html $(PHP_UNIT_COVERAGE_DIR)#v #~~ additional flags passed when collecting coverage
994
+
995
+ _PEST_BINARY := $(WORKDIR)/vendor/bin/pest
996
+ _IS_PEST = $(findstring pest,$(PHP_UNIT))
997
+ _PHP_UNIT_UPDATE = $(if $(wildcard $(CURDIR)/vendor/bin/update-snapshots),$(subst /vendor/bin/phpunit,/vendor/bin/update-snapshots,$(PHP_UNIT)),$(PHP_UNIT))
998
+ _PHP_UNIT_FLOOR = $(filter-out 0,$(strip $(PHP_UNIT_MIN_COVERAGE_LINES)))
999
+
1000
+ _PHP_UNIT_FLOORS = Classes=$(strip $(PHP_UNIT_MIN_COVERAGE_CLASSES)) \
1001
+ Methods=$(strip $(PHP_UNIT_MIN_COVERAGE_METHODS)) \
1002
+ Lines=$(strip $(PHP_UNIT_MIN_COVERAGE_LINES))
1003
+
1004
+ _TEST_NAMES += $(if $(wildcard $(CURDIR)/tests),$(call _when_tracked,phpunit,$(PHP_EXTENSIONS)))
1005
+ _COVERAGE_NAMES += $(if $(wildcard $(CURDIR)/tests),$(call _when_tracked,phpunit,$(PHP_EXTENSIONS)))
1006
+ _LAYERED_CONFIGS += $(if $(wildcard $(CURDIR)/tests),$(call _when_tracked,$(call _layered,$(PHP_UNIT_CONFIG),xml),$(PHP_EXTENSIONS)))
1007
+
1008
+ _$(_TARGET_PREFIX)assert-pest:
1009
+ $(TRACE_PREFIX)test -f '$(call _host_path,$(_PEST_BINARY))' || { \
1010
+ $(call log,`pest` is not installed.,$(COLOR_ERROR)) >&2; \
1011
+ $(call log,Require `pestphp/pest` or run `make $(call _target,phpunit)` to use the runner this project has.,$(COLOR_NOTICE)) >&2; \
1012
+ exit $(BRNSHKR_CONFIG_ERROR_CODE); \
1013
+ }
1014
+
1015
+ $(call _target,pest): _$(_TARGET_PREFIX)assert-pest #~~ runs the PHP tests through `pest` in particular #vvv
1016
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,phpunit) PHP_UNIT='$(PEST)' -- $(ARGS)
1017
+
1018
+ $(call _target,pest-coverage): _$(_TARGET_PREFIX)assert-pest #~~ runs them through `pest` and reports its coverage #vvv
1019
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,phpunit-coverage) PHP_UNIT='$(PEST)' -- $(ARGS)
1020
+
1021
+ $(call _target,pest-debug): _$(_TARGET_PREFIX)assert-pest #~~ runs them through `pest` with debugging information #vvv
1022
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,phpunit-debug) PHP_UNIT='$(PEST)' -- $(ARGS)
1023
+
1024
+ $(call _target,pest-list): _$(_TARGET_PREFIX)assert-pest #~~ lists the tests `pest` collects #vvv
1025
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,phpunit-list) PHP_UNIT='$(PEST)' -- $(ARGS)
1026
+
1027
+ $(call _target,pest-update): _$(_TARGET_PREFIX)assert-pest #~~ runs them through `pest` and updates their snapshots #vvv
1028
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,phpunit-update) PHP_UNIT='$(PEST)' -- $(ARGS)
1029
+
1030
+ $(call _target,phpunit): #~~ runs the PHP tests
1031
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_UNIT_CONFIG),PHP_UNIT_CONFIG)
1032
+ $(DEBUG_PREFIX)$(call _spinner,$(PHP_UNIT) --configuration $(PHP_UNIT_CONFIG) $(PHP_UNIT_FLAGS) $(ARGS))
1033
+
1034
+ $(call _target,phpunit-coverage): #~~ runs the PHP tests and reports how much of the source they reach #v
1035
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_UNIT_CONFIG),PHP_UNIT_CONFIG)
1036
+ $(DEBUG_PREFIX)$(call _spinner,$(PHP_UNIT) --configuration $(PHP_UNIT_CONFIG) $(PHP_UNIT_FLAGS) $(PHP_UNIT_COVERAGE_FLAGS) $(ARGS))
1037
+ $(TRACE_PREFIX)$(if $(or $(_IS_PEST),$(if $(_PHP_UNIT_FLOORS),,1)),:,$(foreach FLOOR,$(_PHP_UNIT_FLOORS),$(call _require_coverage,$(call _host_path,$(PHP_UNIT_COVERAGE_FILE)),$(firstword $(subst =, ,$(FLOOR))),$(lastword $(subst =, ,$(FLOOR))),PHP_UNIT_COVERAGE_FILE)$(_NEWLINE)))
1038
+
1039
+ $(call _target,phpunit-debug): #~~ runs the PHP tests with debugging information instead of progress #vvv
1040
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,phpunit) -- --debug $(ARGS)
1041
+
1042
+ $(call _target,phpunit-list): #~~ lists the tests the runner collects #vv
1043
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_UNIT_CONFIG),PHP_UNIT_CONFIG)
1044
+ $(DEBUG_PREFIX)$(PHP_UNIT) --configuration $(PHP_UNIT_CONFIG) $(ARGS) --list-tests
1045
+
1046
+ $(call _target,phpunit-update): #~~ runs the PHP tests and updates their snapshots #v
1047
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_UNIT_CONFIG),PHP_UNIT_CONFIG)
1048
+ $(DEBUG_PREFIX)$(_PHP_UNIT_UPDATE) --configuration $(PHP_UNIT_CONFIG) $(PHP_UNIT_FLAGS) $(PHP_UNIT_SNAPSHOT_FLAGS) $(ARGS)
1049
+
1050
+ endif # /phpunit
1051
+
1052
+ #--- phpstan
1053
+
1054
+ ifneq ($(wildcard $(CURDIR)/vendor/bin/phpstan),) # phpstan
1055
+
1056
+ PHP_STAN ?= $(_RUN) $(WORKDIR)/vendor/bin/phpstan#v #~~ path to `phpstan` binary
1057
+ PHP_STAN_CONFIG ?= $(call _config,$(WORKDIR)/conf/phpstan.php $(WORKDIR)/conf/phpstan.dist.php,php)#v #~~ path to `phpstan` config
1058
+ PHP_STAN_FLAGS ?= --memory-limit=-1 $(_NO_ANSI_OPTION) $(if $(_IS_DEBUG),-vvv,-vv)#v #~~ additional flags passed to `phpstan`
1059
+
1060
+ _ANALYZER_NAMES += $(call _when_tracked,phpstan,$(PHP_EXTENSIONS))
1061
+ _GROUP_NAMES += $(call _when_tracked,phpstan,$(PHP_EXTENSIONS))
1062
+ _LAYERED_CONFIGS += $(call _when_tracked,$(call _layered,$(PHP_STAN_CONFIG),php),$(PHP_EXTENSIONS))
1063
+
1064
+ $(call _target,phpstan): #~~ runs `phpstan analyze`
1065
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_STAN_CONFIG),PHP_STAN_CONFIG)
1066
+ $(DEBUG_PREFIX)$(call _spinner,$(PHP_STAN) analyze --configuration $(PHP_STAN_CONFIG) $(PHP_STAN_FLAGS) $(ARGS))
1067
+
1068
+ $(call _target,phpstan-debug): #~~ runs `phpstan analyze` in debug mode #vvv
1069
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,phpstan) -- --debug $(ARGS)
1070
+
1071
+ $(call _target,phpstan-group): #~~ counts the `phpstan` findings by identifier #v
1072
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_STAN_CONFIG),PHP_STAN_CONFIG)
1073
+ $(DEBUG_PREFIX)$(call _group,$(PHP_STAN) analyze --configuration $(PHP_STAN_CONFIG) $(PHP_STAN_FLAGS) --no-progress --error-format=json $(ARGS),identifier,message)
1074
+
1075
+ $(call _target,phpstan-list): #~~ lists all files that are processed by `phpstan` #vv
1076
+ $(DEBUG_PREFIX)$(call _capture,$(MAKE) $(_MAKE_FLAGS) $(call _target,phpstan-raw) -- --debug 2>&1) \
1077
+ | $(AWK) 'match($$0, /.+\.php$$/) { print substr($$0, RSTART, RLENGTH) }'
1078
+
1079
+ $(call _target,phpstan-print): #~~ prints the resolved `phpstan` configuration #vv
1080
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_STAN_CONFIG),PHP_STAN_CONFIG)
1081
+ $(DEBUG_PREFIX)$(PHP_STAN) dump-parameters --configuration $(PHP_STAN_CONFIG) $(_NO_ANSI_OPTION) $(ARGS)
1082
+
1083
+ $(call _target,phpstan-raw): #~~ runs `phpstan analyze` with raw output #vvv
1084
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_STAN_CONFIG),PHP_STAN_CONFIG)
1085
+ $(DEBUG_PREFIX)$(PHP_STAN) analyze --configuration $(PHP_STAN_CONFIG) $(PHP_STAN_FLAGS) --error-format=raw $(ARGS)
1086
+
1087
+ endif # /phpstan
1088
+
1089
+ #--- php-cs-fixer
1090
+
1091
+ ifneq ($(wildcard $(CURDIR)/vendor/bin/php-cs-fixer),) # php-cs-fixer
1092
+
1093
+ PHP_CS_FIXER ?= $(_RUN) $(WORKDIR)/vendor/bin/php-cs-fixer#v #~~ path to `php-cs-fixer` binary
1094
+ PHP_CS_FIXER_CONFIG ?= $(call _config,$(WORKDIR)/conf/php-cs-fixer.php $(WORKDIR)/conf/php-cs-fixer.dist.php,php)#v #~~ path to `php-cs-fixer` config
1095
+ PHP_CS_FIXER_FLAGS ?= --show-progress=dots $(_NO_ANSI_OPTION) $(if $(_IS_DEBUG),-vvv,-v)#v #~~ additional flags passed to `php-cs-fixer`
1096
+
1097
+ _FIXER_NAMES += $(call _when_tracked,php-cs-fixer,$(PHP_EXTENSIONS))
1098
+ _GROUP_NAMES += $(call _when_tracked,php-cs-fixer,$(PHP_EXTENSIONS))
1099
+ _LAYERED_CONFIGS += $(call _when_tracked,$(call _layered,$(PHP_CS_FIXER_CONFIG),php),$(PHP_EXTENSIONS))
1100
+
1101
+ $(call _target,php-cs-fixer): #~~ runs `php-cs-fixer fix`
1102
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_CS_FIXER_CONFIG),PHP_CS_FIXER_CONFIG)
1103
+ $(DEBUG_PREFIX)$(call _spinner,$(PHP_CS_FIXER) fix --config $(PHP_CS_FIXER_CONFIG) $(PHP_CS_FIXER_FLAGS) $(ARGS))
1104
+
1105
+ $(call _target,php-cs-fixer-dry-run): #~~ runs `php-cs-fixer fix` in dry run mode #v
1106
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,php-cs-fixer) -- --dry-run $(ARGS)
1107
+
1108
+ $(call _target,php-cs-fixer-group): #~~ counts the `php-cs-fixer` findings by fixer #v
1109
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_CS_FIXER_CONFIG),PHP_CS_FIXER_CONFIG)
1110
+ $(DEBUG_PREFIX)$(call _group,$(PHP_CS_FIXER) fix --config $(PHP_CS_FIXER_CONFIG) $(PHP_CS_FIXER_FLAGS) $(ARGS) --dry-run --format=json,appliedFixers)
1111
+
1112
+ $(call _target,php-cs-fixer-list): #~~ lists all files that are processed by `php-cs-fixer` #vv
1113
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_CS_FIXER_CONFIG),PHP_CS_FIXER_CONFIG)
1114
+ $(DEBUG_PREFIX)$(call _capture,$(PHP_CS_FIXER) list-files --config $(PHP_CS_FIXER_CONFIG) $(_NO_ANSI_OPTION) 2>&1) \
1115
+ | $(AWK) '/^'\''\.\/.*\.php'\''$$/ { sub(/^'\''\.\//, ""); sub(/'\''$$/, ""); print }'
1116
+
1117
+ $(call _target,php-cs-fixer-print): #~~ prints the `php-cs-fixer` rules in use #vv
1118
+ $(TRACE_PREFIX)$(call _require_file,$(PHP_CS_FIXER_CONFIG),PHP_CS_FIXER_CONFIG)
1119
+ $(DEBUG_PREFIX)$(PHP_CS_FIXER) describe --config $(PHP_CS_FIXER_CONFIG) $(_NO_ANSI_OPTION) -n @ $(ARGS)
1120
+
1121
+ endif # /php-cs-fixer
1122
+
1123
+ #--- rector
1124
+
1125
+ ifneq ($(wildcard $(CURDIR)/vendor/bin/rector),) # rector
1126
+
1127
+ RECTOR ?= $(_RUN) $(WORKDIR)/vendor/bin/rector#v #~~ path to `rector` binary
1128
+ RECTOR_CONFIG ?= $(call _config,$(WORKDIR)/conf/rector.php $(WORKDIR)/conf/rector.dist.php,php)#v #~~ path to `rector` config
1129
+ RECTOR_FLAGS ?= --memory-limit=-1 $(_NO_ANSI_OPTION)#v #~~ additional flags passed to `rector`
1130
+
1131
+ _FIXER_NAMES += $(call _when_tracked,rector,$(PHP_EXTENSIONS))
1132
+ _GROUP_NAMES += $(call _when_tracked,rector,$(PHP_EXTENSIONS))
1133
+ _LAYERED_CONFIGS += $(call _when_tracked,$(call _layered,$(RECTOR_CONFIG),php),$(PHP_EXTENSIONS))
1134
+
1135
+ $(call _target,rector): #~~ runs `rector process`
1136
+ $(TRACE_PREFIX)$(call _require_file,$(RECTOR_CONFIG),RECTOR_CONFIG)
1137
+ $(DEBUG_PREFIX)$(call _spinner,$(RECTOR) process --config $(RECTOR_CONFIG) $(RECTOR_FLAGS) $(ARGS))
1138
+
1139
+ $(call _target,rector-debug): #~~ runs `rector process` in debug mode #vvv
1140
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,rector) -- --debug $(ARGS)
1141
+
1142
+ $(call _target,rector-dry-run): #~~ runs `rector process` in dry run mode #v
1143
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,rector) -- --dry-run $(ARGS)
1144
+
1145
+ $(call _target,rector-group): #~~ counts the `rector` findings by rule #v
1146
+ $(TRACE_PREFIX)$(call _require_file,$(RECTOR_CONFIG),RECTOR_CONFIG)
1147
+ $(DEBUG_PREFIX)$(call _group,$(RECTOR) process --config $(RECTOR_CONFIG) $(RECTOR_FLAGS) $(ARGS) --dry-run --output-format=json,applied_rectors)
1148
+
1149
+ $(call _target,rector-list): #~~ lists all files that are processed by `rector` #vv
1150
+ $(TRACE_PREFIX)$(call _require_file,$(RECTOR_CONFIG),RECTOR_CONFIG)
1151
+ $(DEBUG_PREFIX)$(call _capture,$(RECTOR) process --config $(RECTOR_CONFIG) --debug --dry-run) | $(AWK) '/\[file\]/ { sub(".*\\[file\\] $(WORKDIR)/", ""); if (/\.php$$/) print }'
1152
+
1153
+ $(call _target,rector-print): #~~ prints the `rector` rules in use #vv
1154
+ $(TRACE_PREFIX)$(call _require_file,$(RECTOR_CONFIG),RECTOR_CONFIG)
1155
+ $(DEBUG_PREFIX)$(RECTOR) list-rules --config $(RECTOR_CONFIG) $(_NO_ANSI_OPTION) $(ARGS)
1156
+
1157
+ endif # /rector
1158
+
1159
+ #--- twig-cs-fixer
1160
+
1161
+ ifneq ($(wildcard $(CURDIR)/vendor/bin/twig-cs-fixer),) # twig-cs-fixer
1162
+
1163
+ TWIG_CS_FIXER ?= $(_RUN) $(WORKDIR)/vendor/bin/twig-cs-fixer#v #~~ path to `twig-cs-fixer` binary
1164
+ TWIG_CS_FIXER_CONFIG ?= $(call _config,$(WORKDIR)/conf/twig-cs-fixer.php $(WORKDIR)/conf/twig-cs-fixer.dist.php,php)#v #~~ path to `twig-cs-fixer` config
1165
+ TWIG_CS_FIXER_FLAGS ?= -v $(_NO_ANSI_OPTION)#v #~~ additional flags passed to `twig-cs-fixer`
1166
+ TWIG_CS_FIXER_EXTENSIONS ?= twig#vv #~~ extensions `twig-cs-fixer` reads
1167
+
1168
+ _FIXER_NAMES += $(call _when_tracked,twig-cs-fixer,$(TWIG_CS_FIXER_EXTENSIONS))
1169
+ _GROUP_NAMES += $(call _when_tracked,twig-cs-fixer,$(TWIG_CS_FIXER_EXTENSIONS))
1170
+ _LAYERED_CONFIGS += $(call _when_tracked,$(call _layered,$(TWIG_CS_FIXER_CONFIG),php),$(TWIG_CS_FIXER_EXTENSIONS))
1171
+
1172
+ $(call _target,twig-cs-fixer): #~~ runs `twig-cs-fixer fix`, or lints only when `--dry-run` is passed
1173
+ $(TRACE_PREFIX)$(call _require_file,$(TWIG_CS_FIXER_CONFIG),TWIG_CS_FIXER_CONFIG)
1174
+ $(DEBUG_PREFIX)$(call _spinner,$(TWIG_CS_FIXER) lint $(if $(call _is_dry_run,$(_ARGS)),,--fix) --config $(TWIG_CS_FIXER_CONFIG) $(TWIG_CS_FIXER_FLAGS) $(_FIXER_ARGS))
1175
+
1176
+ $(call _target,twig-cs-fixer-debug): #~~ runs `twig-cs-fixer` in debug mode #vvv
1177
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,twig-cs-fixer) -- --debug $(ARGS)
1178
+
1179
+ $(call _target,twig-cs-fixer-dry-run): #~~ runs `twig-cs-fixer` in dry run mode #v
1180
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,twig-cs-fixer) -- --dry-run $(ARGS)
1181
+
1182
+ $(call _target,twig-cs-fixer-group): #~~ counts the `twig-cs-fixer` findings by rule #v
1183
+ $(TRACE_PREFIX)$(call _require_file,$(TWIG_CS_FIXER_CONFIG),TWIG_CS_FIXER_CONFIG)
1184
+ $(DEBUG_PREFIX)$(call _group,$(TWIG_CS_FIXER) lint --config $(TWIG_CS_FIXER_CONFIG) --report gitlab $(ARGS),check_name,description)
1185
+
1186
+ endif # /twig-cs-fixer
1187
+
1188
+ endif # /php
1189
+
1190
+ ifneq ($(wildcard $(CURDIR)/package.json),) # js
1191
+
1192
+ #--- bun
1193
+
1194
+ BUN ?= $(_RUN) bun#v #~~ command that runs `bun`
1195
+ BUN_FLAGS ?= --bun#v #~~ flags passed to `bun` before the command
1196
+
1197
+ _BUN_PACKAGE_DIR := $(CURDIR)/.local/$(if $(VENDOR),$(VENDOR)/)$(PACKAGE)
1198
+ _BUN_INSTALL = $(BUN) install
1199
+
1200
+ _PACK_NAMES += bun-pack
1201
+
1202
+ $(call _target,bun): #~~ runs `bun`
1203
+ $(DEBUG_PREFIX)$(BUN) $(ARGS)
1204
+
1205
+ $(call _target,bun-list): #~~ lists the files the bun package would ship #vv
1206
+ $(DEBUG_PREFIX)$(call _capture,NO_COLOR=1 $(BUN) pm pack --dry-run) | $(AWK) '/^packed /{ print $$3 }'
1207
+
1208
+ $(call _target,bun-pack): #~~ packs the bun package into `./.local/<VENDOR>/<PACKAGE>` #vvv
1209
+ $(DEBUG_PREFIX)$(RM) -rf $(_BUN_PACKAGE_DIR) \
1210
+ && $(MKDIR) -p $(_BUN_PACKAGE_DIR) \
1211
+ && $(BUN) pm pack --quiet --filename package.tgz >/dev/null \
1212
+ && $(TAR) -xzf package.tgz -C $(_BUN_PACKAGE_DIR) --strip-components 1 \
1213
+ && $(RM) -f package.tgz
1214
+
1215
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/node-modules-inspector),) # bun-inspect
1216
+
1217
+ NODE_MODULES_INSPECTOR ?= $(BUN) $(BUN_FLAGS) x node-modules-inspector#v #~~ command that runs `node-modules-inspector`
1218
+ NODE_MODULES_INSPECTOR_FLAGS ?= #v #~~ additional flags passed to `node-modules-inspector`
1219
+
1220
+ $(call _target,bun-inspect): #~~ runs `node-modules-inspector` #v
1221
+ $(DEBUG_PREFIX)$(NODE_MODULES_INSPECTOR) $(ARGS) $(NODE_MODULES_INSPECTOR_FLAGS)
1222
+
1223
+ endif # /bun-inspect
1224
+
1225
+ _$(_TARGET_PREFIX)bun-install:
1226
+ $(DEBUG_PREFIX)$(_BUN_INSTALL)
1227
+
1228
+ #--- commitlint
1229
+
1230
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/commitlint),) # commitlint
1231
+
1232
+ COMMITLINT ?= $(BUN) $(BUN_FLAGS) x commitlint#v #~~ command that runs `commitlint`
1233
+ COMMITLINT_CONFIG ?= $(call _config,$(WORKDIR)/conf/commitlint.mjs $(WORKDIR)/conf/commitlint.dist.mjs,js)#v #~~ path to `commitlint` config
1234
+ COMMITLINT_SOURCE ?= $(if $(wildcard $(CURDIR)/.git/COMMIT_EDITMSG),--edit,--last)#vv #~~ which message `commitlint` reads
1235
+ COMMITLINT_FLAGS ?= $(if $(_IS_DEBUG),--verbose) $(if $(_IS_NO_ANSI),--color false)#v #~~ additional flags passed to `commitlint`
1236
+
1237
+ GIT_HOOKS += commit-msg=$(call _target,commitlint)
1238
+
1239
+ _ANALYZER_NAMES += commitlint
1240
+ _LAYERED_CONFIGS += $(call _layered,$(COMMITLINT_CONFIG),mjs)
1241
+
1242
+ $(call _target,commitlint): _$(_TARGET_PREFIX)install-missing-hooks #~~ lints a commit message
1243
+ $(TRACE_PREFIX)$(call _require_file,$(COMMITLINT_CONFIG),COMMITLINT_CONFIG)
1244
+ $(DEBUG_PREFIX)$(call _spinner,$(COMMITLINT) --config $(COMMITLINT_CONFIG) $(COMMITLINT_FLAGS) $(if $(ARGS),$(ARGS),$(COMMITLINT_SOURCE)))
1245
+
1246
+ $(call _target,commitlint-print): #~~ prints the resolved `commitlint` configuration #vv
1247
+ $(TRACE_PREFIX)$(call _require_file,$(COMMITLINT_CONFIG),COMMITLINT_CONFIG)
1248
+ $(DEBUG_PREFIX)$(COMMITLINT) --config $(COMMITLINT_CONFIG) $(COMMITLINT_FLAGS) --print-config $(ARGS)
1249
+
1250
+ endif # /commitlint
1251
+
1252
+ #--- eslint
1253
+
1254
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/eslint),) # eslint
1255
+
1256
+ ESLINT ?= $(BUN) $(BUN_FLAGS) x eslint#v #~~ command that runs `eslint`
1257
+ ESLINT_CONFIG ?= $(call _config,$(WORKDIR)/conf/eslint.ts $(WORKDIR)/conf/eslint.mjs $(WORKDIR)/conf/eslint.dist.mjs,js)#v #~~ path to `eslint` config
1258
+ ESLINT_CACHE ?= $(WORKDIR)/.cache/eslint.cache.json#v #~~ path to the `eslint` cache
1259
+ ESLINT_FLAGS ?= --max-warnings 0 $(if $(_IS_DEBUG),--debug) $(if $(_IS_NO_ANSI),--no-color)#v #~~ additional flags passed to `eslint`
1260
+ unexport ESLINT_FLAGS # NOTICE: ESLint reads this as its feature flags
1261
+
1262
+ _ESLINT_OUTPUT := $(WORKDIR)/.cache/eslint.output
1263
+
1264
+ _ESLINT_EXTENSIONS := js cjs mjs jsx \
1265
+ $(call _when_installed,ts cts mts tsx,node_modules/typescript-eslint) \
1266
+ $(call _when_installed,svelte,node_modules/eslint-plugin-svelte) \
1267
+ $(call _when_installed,json,node_modules/@eslint/json) \
1268
+ $(call _when_installed,jsonc json5,node_modules/eslint-plugin-jsonc) \
1269
+ $(call _when_installed,yml yaml,node_modules/eslint-plugin-yml) \
1270
+ $(call _when_installed,toml,node_modules/eslint-plugin-toml) \
1271
+ $(call _when_installed,css,node_modules/@eslint/css) \
1272
+ $(call _when_installed,md,node_modules/@eslint/markdown)
1273
+
1274
+ ESLINT_EXTENSIONS ?= $(_ESLINT_EXTENSIONS)#vv #~~ extensions `eslint` lints, which follows the modules its config enables
1275
+
1276
+ _FIXER_NAMES += $(call _when_tracked,eslint,$(ESLINT_EXTENSIONS))
1277
+ _GROUP_NAMES += $(call _when_tracked,eslint,$(ESLINT_EXTENSIONS))
1278
+ _LAYERED_CONFIGS += $(call _when_tracked,$(call _layered,$(ESLINT_CONFIG),mjs),$(ESLINT_EXTENSIONS))
1279
+
1280
+ # NOTICE: ESLint writes through `--output-file` since Bun truncates its piped output
1281
+ $(call _target,eslint): #~~ runs `eslint`, and writes its fixes unless `--dry-run` is passed
1282
+ $(TRACE_PREFIX)$(call _require_file,$(ESLINT_CONFIG),ESLINT_CONFIG)
1283
+ $(DEBUG_PREFIX)$(call _spinner,$(RM) -f '$(call _host_path,$(_ESLINT_OUTPUT))'; \
1284
+ $(ESLINT) --config $(ESLINT_CONFIG) --cache --cache-location $(ESLINT_CACHE) $(ESLINT_FLAGS) --output-file $(_ESLINT_OUTPUT) $(_FIXER_ARGS) $(if $(call _is_dry_run,$(_ARGS)),,--fix); \
1285
+ status=$$?; \
1286
+ [ ! -f '$(call _host_path,$(_ESLINT_OUTPUT))' ] || $(CAT) '$(call _host_path,$(_ESLINT_OUTPUT))'; \
1287
+ $(RM) -f '$(call _host_path,$(_ESLINT_OUTPUT))'; \
1288
+ exit $$status)
1289
+
1290
+ $(call _target,eslint-dry-run): #~~ runs `eslint` without writing fixes #v
1291
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,eslint) -- --dry-run $(ARGS)
1292
+
1293
+ $(call _target,eslint-group): #~~ counts the `eslint` findings by rule #v
1294
+ $(TRACE_PREFIX)$(call _require_file,$(ESLINT_CONFIG),ESLINT_CONFIG)
1295
+ $(DEBUG_PREFIX)$(call _group,$(ESLINT) --config $(ESLINT_CONFIG) --cache --cache-location $(ESLINT_CACHE) $(ESLINT_FLAGS) --format json --output-file /dev/stdout $(ARGS),ruleId,message)
1296
+
1297
+ $(call _target,eslint-print): #~~ prints the resolved `eslint` config for a file #vv
1298
+ $(TRACE_PREFIX)$(call _require_file,$(ESLINT_CONFIG),ESLINT_CONFIG)
1299
+ $(DEBUG_PREFIX)$(ESLINT) --config $(ESLINT_CONFIG) --print-config $(ARGS)
1300
+
1301
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/eslint-config-inspector),) # eslint-inspect
1302
+
1303
+ # NOTICE: runs on node since bun has no `module.registerHooks` (https://github.com/oven-sh/bun/issues/27369)
1304
+ ESLINT_CONFIG_INSPECTOR ?= $(BUN) x eslint-config-inspector#v #~~ command that runs `eslint-config-inspector`
1305
+ ESLINT_CONFIG_INSPECTOR_FLAGS ?= #v #~~ additional flags passed to `eslint-config-inspector`
1306
+
1307
+ $(call _target,eslint-inspect): #~~ runs `eslint-config-inspector` #v
1308
+ $(TRACE_PREFIX)$(call _require_file,$(ESLINT_CONFIG),ESLINT_CONFIG)
1309
+ $(DEBUG_PREFIX)$(ESLINT_CONFIG_INSPECTOR) --config $(ESLINT_CONFIG) $(ARGS) $(ESLINT_CONFIG_INSPECTOR_FLAGS)
1310
+
1311
+ $(call _target,eslint-inspect-stats): #~~ runs `eslint-config-inspector` and times every rule with a full lint on startup #vv
1312
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,eslint-inspect) -- $(ARGS) --stats
1313
+
1314
+ endif # /eslint-inspect
1315
+
1316
+ endif # /eslint
1317
+
1318
+ #--- markdownlint
1319
+
1320
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/markdownlint-cli2),) # markdownlint
1321
+
1322
+ MARKDOWNLINT ?= $(BUN) $(BUN_FLAGS) x markdownlint-cli2#v #~~ command that runs `markdownlint-cli2`
1323
+ MARKDOWNLINT_CONFIG ?= $(call _config,$(WORKDIR)/conf/markdownlint.mjs $(WORKDIR)/conf/markdownlint.dist.mjs,js)#v #~~ path to `markdownlint` config
1324
+ MARKDOWNLINT_GLOB ?= $(call _brace_glob,$(MARKDOWNLINT_EXTENSIONS))#vv #~~ files `markdownlint` looks at
1325
+ MARKDOWNLINT_FLAGS ?= $(if $(_IS_NO_ANSI),--no-color)#v #~~ additional flags passed to `markdownlint-cli2`
1326
+ MARKDOWNLINT_EXTENSIONS ?= md markdown#vv #~~ extensions `markdownlint` reads
1327
+
1328
+ _MARKDOWNLINT_VALUE_FLAGS := --config \
1329
+ --configPointer
1330
+
1331
+ _FIXER_NAMES += $(call _when_tracked,markdownlint,$(MARKDOWNLINT_EXTENSIONS))
1332
+ _GROUP_NAMES += $(call _when_tracked,markdownlint,$(MARKDOWNLINT_EXTENSIONS))
1333
+ _LAYERED_CONFIGS += $(call _when_tracked,$(call _layered,$(MARKDOWNLINT_CONFIG),mjs),$(MARKDOWNLINT_EXTENSIONS))
1334
+
1335
+ $(call _target,markdownlint): #~~ runs `markdownlint-cli2`, and writes its fixes unless `--dry-run` is passed
1336
+ $(TRACE_PREFIX)$(call _require_file,$(MARKDOWNLINT_CONFIG),MARKDOWNLINT_CONFIG)
1337
+ $(DEBUG_PREFIX)$(call _spinner,$(MARKDOWNLINT) --config $(MARKDOWNLINT_CONFIG) $(MARKDOWNLINT_FLAGS) \
1338
+ $(if $(call _is_dry_run,$(_ARGS)),,--fix) \
1339
+ $(if $(call _has_positional_arg,$(call _without_dry_run,$(_ARGS)),$(_MARKDOWNLINT_VALUE_FLAGS)), \
1340
+ $(_FIXER_ARGS), \
1341
+ $(_FIXER_ARGS) '$(MARKDOWNLINT_GLOB)'))
1342
+
1343
+ $(call _target,markdownlint-dry-run): #~~ runs `markdownlint-cli2` without writing fixes #v
1344
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,markdownlint) -- --dry-run $(ARGS)
1345
+
1346
+ $(call _target,markdownlint-group): #~~ counts the `markdownlint` findings by rule #v
1347
+ $(TRACE_PREFIX)$(call _require_file,$(MARKDOWNLINT_CONFIG),MARKDOWNLINT_CONFIG)
1348
+ $(DEBUG_PREFIX)$(call _group,$(MARKDOWNLINT) --config $(MARKDOWNLINT_CONFIG) $(MARKDOWNLINT_FLAGS) $(if $(call _has_positional_arg,$(ARGS),$(_MARKDOWNLINT_VALUE_FLAGS)),$(ARGS),$(ARGS) '$(MARKDOWNLINT_GLOB)'))
1349
+
1350
+ endif # /markdownlint
1351
+
1352
+ #--- stylelint
1353
+
1354
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/stylelint),) # stylelint
1355
+
1356
+ STYLELINT ?= $(BUN) $(BUN_FLAGS) x stylelint#v #~~ command that runs `stylelint`
1357
+ STYLELINT_CONFIG ?= $(call _config,$(WORKDIR)/conf/stylelint.mjs $(WORKDIR)/conf/stylelint.dist.mjs,js)#v #~~ path to `stylelint` config
1358
+ STYLELINT_GLOB ?= $(call _brace_glob,$(STYLELINT_EXTENSIONS))#vv #~~ files `stylelint` looks at
1359
+ STYLELINT_FLAGS ?= --allow-empty-input --max-warnings 0 $(if $(_IS_NO_ANSI),--no-color)#v #~~ additional flags passed to `stylelint`
1360
+
1361
+ _STYLELINT_VALUE_FLAGS := --cache-location \
1362
+ --cache-strategy \
1363
+ --config-basedir \
1364
+ --config \
1365
+ --custom-formatter \
1366
+ --custom-syntax \
1367
+ --formatter \
1368
+ --globby-options \
1369
+ --go \
1370
+ --ignore-path \
1371
+ --ignore-pattern \
1372
+ --ip \
1373
+ --max-warnings \
1374
+ --mw \
1375
+ --output-file \
1376
+ --stdin-filename \
1377
+ --suppress-location \
1378
+ -c \
1379
+ -f \
1380
+ -i \
1381
+ -o
1382
+
1383
+ _STYLELINT_EXTENSIONS := css \
1384
+ $(call _when_installed,less,node_modules/stylelint-config-standard-less) \
1385
+ $(call _when_installed,scss,node_modules/stylelint-config-standard-scss) \
1386
+ $(call _when_installed,ejs html svelte svg vue astro xml,node_modules/stylelint-config-html)
1387
+
1388
+ STYLELINT_EXTENSIONS ?= $(_STYLELINT_EXTENSIONS)#vv #~~ extensions `stylelint` can parse, which follows the syntaxes its config pulls in
1389
+
1390
+ _FIXER_NAMES += $(call _when_tracked,stylelint,$(STYLELINT_EXTENSIONS))
1391
+ _GROUP_NAMES += $(call _when_tracked,stylelint,$(STYLELINT_EXTENSIONS))
1392
+ _LAYERED_CONFIGS += $(call _when_tracked,$(call _layered,$(STYLELINT_CONFIG),mjs),$(STYLELINT_EXTENSIONS))
1393
+
1394
+ $(call _target,stylelint): #~~ runs `stylelint`, and writes its fixes unless `--dry-run` is passed
1395
+ $(TRACE_PREFIX)$(call _require_file,$(STYLELINT_CONFIG),STYLELINT_CONFIG)
1396
+ $(DEBUG_PREFIX)$(call _spinner,$(STYLELINT) --config $(STYLELINT_CONFIG) $(STYLELINT_FLAGS) \
1397
+ $(if $(call _is_dry_run,$(_ARGS)),,--fix) \
1398
+ $(if $(call _has_positional_arg,$(call _without_dry_run,$(_ARGS)),$(_STYLELINT_VALUE_FLAGS)), \
1399
+ $(_FIXER_ARGS), \
1400
+ $(_FIXER_ARGS) '$(STYLELINT_GLOB)'))
1401
+
1402
+ $(call _target,stylelint-dry-run): #~~ runs `stylelint` without writing fixes #v
1403
+ $(DEBUG_PREFIX)$(MAKE) $(_MAKE_FLAGS) $(call _target,stylelint) -- --dry-run $(ARGS)
1404
+
1405
+ $(call _target,stylelint-group): #~~ counts the `stylelint` findings by rule #v
1406
+ $(TRACE_PREFIX)$(call _require_file,$(STYLELINT_CONFIG),STYLELINT_CONFIG)
1407
+ $(DEBUG_PREFIX)$(call _group,$(STYLELINT) --config $(STYLELINT_CONFIG) $(STYLELINT_FLAGS) --formatter json $(if $(call _has_positional_arg,$(ARGS),$(_STYLELINT_VALUE_FLAGS)),$(ARGS),$(ARGS) '$(STYLELINT_GLOB)'),rule,text)
1408
+
1409
+ $(call _target,stylelint-print): #~~ prints the resolved `stylelint` config for a file #vv
1410
+ $(TRACE_PREFIX)$(call _require_file,$(STYLELINT_CONFIG),STYLELINT_CONFIG)
1411
+ $(DEBUG_PREFIX)$(STYLELINT) --config $(STYLELINT_CONFIG) --print-config $(ARGS)
1412
+
1413
+ endif # /stylelint
1414
+
1415
+ #--- typescript
1416
+
1417
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/tsc),) # typescript
1418
+
1419
+ TYPESCRIPT ?= $(BUN) $(BUN_FLAGS) x tsc#v #~~ command that runs `tsc`
1420
+ TYPESCRIPT_CONFIG ?= $(WORKDIR)/tsconfig.json#v #~~ path to the `tsc` project
1421
+ TYPESCRIPT_FLAGS ?= --noEmit#v #~~ additional flags passed to `tsc`
1422
+ TYPESCRIPT_EXTENSIONS ?= ts tsx cts mts#vv #~~ extensions `tsc` type checks
1423
+
1424
+ _TYPESCRIPT_PROJECT := $(WORKDIR)/conf/tsconfig.json
1425
+
1426
+ _ANALYZER_NAMES += $(call _when_tracked,typescript,$(TYPESCRIPT_EXTENSIONS))
1427
+ _GROUP_NAMES += $(call _when_tracked,typescript,$(TYPESCRIPT_EXTENSIONS))
1428
+ _PLAIN_CONFIGS += $(call _when_tracked,$(_TYPESCRIPT_PROJECT),$(TYPESCRIPT_EXTENSIONS))
1429
+ _LINKED_CONFIGS += $(call _when_tracked,$(call _host_path,$(TYPESCRIPT_CONFIG))|$(call _host_path,$(_TYPESCRIPT_PROJECT)),$(TYPESCRIPT_EXTENSIONS))
1430
+
1431
+ $(call _target,typescript): #~~ runs the `tsc` type check
1432
+ $(TRACE_PREFIX)$(call _require_file,$(TYPESCRIPT_CONFIG),TYPESCRIPT_CONFIG)
1433
+ $(DEBUG_PREFIX)$(call _spinner,$(TYPESCRIPT) --project $(TYPESCRIPT_CONFIG) $(TYPESCRIPT_FLAGS) $(ARGS))
1434
+
1435
+ $(call _target,typescript-group): #~~ counts the `tsc` errors by code #v
1436
+ $(TRACE_PREFIX)$(call _require_file,$(TYPESCRIPT_CONFIG),TYPESCRIPT_CONFIG)
1437
+ $(DEBUG_PREFIX)$(call _group,$(TYPESCRIPT) --project $(TYPESCRIPT_CONFIG) $(TYPESCRIPT_FLAGS) --pretty false $(ARGS))
1438
+
1439
+ $(call _target,typescript-list): #~~ lists the files the `tsc` project includes #vv
1440
+ $(TRACE_PREFIX)$(call _require_file,$(TYPESCRIPT_CONFIG),TYPESCRIPT_CONFIG)
1441
+ $(DEBUG_PREFIX)$(TYPESCRIPT) --project $(TYPESCRIPT_CONFIG) --listFilesOnly $(ARGS)
1442
+
1443
+ $(call _target,typescript-print): #~~ prints the resolved `tsc` project #vv
1444
+ $(TRACE_PREFIX)$(call _require_file,$(TYPESCRIPT_CONFIG),TYPESCRIPT_CONFIG)
1445
+ $(DEBUG_PREFIX)$(TYPESCRIPT) --project $(TYPESCRIPT_CONFIG) --showConfig $(ARGS)
1446
+
1447
+ endif # /typescript
1448
+
1449
+ #--- vitest
1450
+
1451
+ ifneq ($(wildcard $(CURDIR)/node_modules/.bin/vitest),) # vitest
1452
+
1453
+ VITEST ?= $(BUN) $(BUN_FLAGS) x vitest#v #~~ command that runs `vitest`
1454
+ unexport VITEST # NOTICE: Vitest sets this to mark its own runs
1455
+ VITEST_CONFIG ?= $(call _config,$(WORKDIR)/conf/vitest.ts $(WORKDIR)/conf/vitest.mjs $(WORKDIR)/conf/vitest.dist.mjs,js)#v #~~ path to `vitest` config
1456
+ VITEST_EXTENSIONS ?= ts tsx js jsx mts cts mjs cjs#vv #~~ extensions `vitest` runs tests from
1457
+ VITEST_FLAGS ?= --run --passWithNoTests $(if $(_IS_DEBUG),--reporter=verbose)#v #~~ additional flags passed to `vitest`
1458
+
1459
+ VITEST_COVERAGE_DIR ?= $(WORKDIR)/.cache/vitest.cache/coverage#v #~~ directory the HTML coverage report is written to
1460
+ VITEST_MIN_COVERAGE ?= 100#v #~~ percentage every metric must reach, `0` to collect coverage without a floor
1461
+ VITEST_MIN_COVERAGE_BRANCHES ?= $(VITEST_MIN_COVERAGE)#vv #~~ percentage the branch metric must reach
1462
+ VITEST_MIN_COVERAGE_FUNCTIONS ?= $(VITEST_MIN_COVERAGE)#vv #~~ percentage the function metric must reach
1463
+ VITEST_MIN_COVERAGE_LINES ?= $(VITEST_MIN_COVERAGE)#vv #~~ percentage the line metric must reach
1464
+ VITEST_MIN_COVERAGE_STATEMENTS ?= $(VITEST_MIN_COVERAGE)#vv #~~ percentage the statement metric must reach
1465
+
1466
+ VITEST_COVERAGE_FLAGS?= --coverage \
1467
+ --coverage.reporter=text \
1468
+ --coverage.reporter=html \
1469
+ --coverage.reportsDirectory=$(VITEST_COVERAGE_DIR) \
1470
+ --coverage.thresholds.branches=$(VITEST_MIN_COVERAGE_BRANCHES) \
1471
+ --coverage.thresholds.functions=$(VITEST_MIN_COVERAGE_FUNCTIONS) \
1472
+ --coverage.thresholds.lines=$(VITEST_MIN_COVERAGE_LINES) \
1473
+ --coverage.thresholds.statements=$(VITEST_MIN_COVERAGE_STATEMENTS)#v #~~ additional flags passed when collecting coverage
1474
+
1475
+ _VITEST_COVERAGE_PROVIDER := $(firstword $(wildcard \
1476
+ $(CURDIR)/node_modules/@vitest/coverage-v8 \
1477
+ $(CURDIR)/node_modules/@vitest/coverage-istanbul \
1478
+ ))
1479
+
1480
+ _TEST_NAMES += $(if $(wildcard $(CURDIR)/tests),$(call _when_tracked,vitest,$(VITEST_EXTENSIONS)))
1481
+ _COVERAGE_NAMES += $(if $(_VITEST_COVERAGE_PROVIDER),$(if $(wildcard $(CURDIR)/tests),$(call _when_tracked,vitest,$(VITEST_EXTENSIONS))))
1482
+ _LAYERED_CONFIGS += $(if $(wildcard $(CURDIR)/tests),$(call _when_tracked,$(call _layered,$(VITEST_CONFIG),mjs),$(VITEST_EXTENSIONS)))
1483
+
1484
+ $(call _target,vitest): #~~ runs `vitest`
1485
+ $(TRACE_PREFIX)$(call _require_file,$(VITEST_CONFIG),VITEST_CONFIG)
1486
+ $(DEBUG_PREFIX)$(call _spinner,$(VITEST) --config $(VITEST_CONFIG) $(VITEST_FLAGS) $(ARGS))
1487
+
1488
+ ifneq ($(_VITEST_COVERAGE_PROVIDER),) # vitest-coverage
1489
+
1490
+ $(call _target,vitest-coverage): #~~ runs `vitest` and reports how much of the source it reaches #v
1491
+ $(TRACE_PREFIX)$(call _require_file,$(VITEST_CONFIG),VITEST_CONFIG)
1492
+ $(DEBUG_PREFIX)$(call _spinner,$(VITEST) --config $(VITEST_CONFIG) $(VITEST_FLAGS) $(VITEST_COVERAGE_FLAGS) $(ARGS))
1493
+
1494
+ endif # /vitest-coverage
1495
+
1496
+ $(call _target,vitest-list): #~~ lists the tests `vitest` collects #vv
1497
+ $(TRACE_PREFIX)$(call _require_file,$(VITEST_CONFIG),VITEST_CONFIG)
1498
+ $(DEBUG_PREFIX)$(VITEST) list --config $(VITEST_CONFIG) $(ARGS)
1499
+
1500
+ $(call _target,vitest-update): #~~ runs `vitest` and updates its snapshots #v
1501
+ $(TRACE_PREFIX)$(call _require_file,$(VITEST_CONFIG),VITEST_CONFIG)
1502
+ $(DEBUG_PREFIX)$(call _spinner,$(VITEST) --config $(VITEST_CONFIG) $(VITEST_FLAGS) $(ARGS) --update)
1503
+
1504
+ endif # /vitest
1505
+
1506
+ endif # /js
1507
+
1508
+ #--- changelog
1509
+
1510
+ CHANGELOG_RANGE ?= #v #~~ commit range the changelog covers
1511
+ CHANGELOG_URL ?= #v #~~ base URL a commit hash links to
1512
+ CHANGELOG_DIR ?= $(CURDIR)/changelog#vv #~~ directory the release notes are written to
1513
+ CHANGELOG_TITLE ?= \# Changelog#vv #~~ heading a new changelog file starts with
1514
+
1515
+ CHANGELOG_SKIPPED_COMMITS ?= chore(hygiene) \
1516
+ chore(release)#vv #~~ `type(scope)` pairs left out of the changelog
1517
+
1518
+ CHANGELOG_SKIPPED_TYPES ?= test \
1519
+ style \
1520
+ build#vv #~~ commit types left out of the changelog
1521
+
1522
+ CHANGELOG_GROUPS ?= dependencies=📦_Dependencies \
1523
+ a11y=♿_Accessibility \
1524
+ admin=🛠️_Admin \
1525
+ ai=🤖_AI \
1526
+ api=🔌_API \
1527
+ assets=🖼️_Assets \
1528
+ auth=🔐_Auth \
1529
+ backend=🧱_Backend \
1530
+ cache=🗃️_Cache \
1531
+ ci=⚙️_CI \
1532
+ cli=⌨️_CLI \
1533
+ composer=📦_Composer \
1534
+ config=🔧_Configuration \
1535
+ db=🗄️_Database \
1536
+ docker=🐳_Docker \
1537
+ docs=📚_Documentation \
1538
+ events=📡_Events \
1539
+ forms=📝_Forms \
1540
+ frontend=🖥️_Frontend \
1541
+ i18n=🌍_Translations \
1542
+ js=☕_JS \
1543
+ logging=🧾_Logging \
1544
+ mail=✉️_Mail \
1545
+ make=🔨_Make \
1546
+ payment=💳_Payment \
1547
+ perf=⚡_Performance \
1548
+ php=🐘_PHP \
1549
+ queue=📬_Queue \
1550
+ readme=📚_Documentation \
1551
+ release=🔖_Release \
1552
+ routing=🧭_Routing \
1553
+ rule=🧩_Rules \
1554
+ schedule=⏰_Schedule \
1555
+ search=🔍_Search \
1556
+ security=🔒_Security \
1557
+ seo=🔎_SEO \
1558
+ storage=💾_Storage \
1559
+ templates=🖌️_Templates \
1560
+ test=🧪_Tests \
1561
+ twig=🌿_Twig \
1562
+ ui=🎨_UI \
1563
+ vscode=💻_Editor \
1564
+ general=🧰_General#vv #~~ headings per scope root, as `<root>=<heading>` pairs, `_` a space
1565
+
1566
+ CHANGELOG_NAMES ?= a11y=a11y \
1567
+ acl=ACL \
1568
+ apcu=APCu \
1569
+ api=API \
1570
+ aws=AWS \
1571
+ bdd=BDD \
1572
+ cdn=CDN \
1573
+ cgi=CGI \
1574
+ cli=CLI \
1575
+ cms=CMS \
1576
+ commitlint=commitlint \
1577
+ cors=CORS \
1578
+ csp=CSP \
1579
+ css=CSS \
1580
+ csv=CSV \
1581
+ db=Database \
1582
+ dbal=DBAL \
1583
+ di=DI \
1584
+ dns=DNS \
1585
+ dto=DTO \
1586
+ dx=DX \
1587
+ esbuild=esbuild \
1588
+ eslint=ESLint \
1589
+ fastcgi=FastCGI \
1590
+ ffi=FFI \
1591
+ fpm=FPM \
1592
+ frankenphp=FrankenPHP \
1593
+ gcp=GCP \
1594
+ github=GitHub \
1595
+ gitlab=GitLab \
1596
+ graphql=GraphQL \
1597
+ grpc=gRPC \
1598
+ gui=GUI \
1599
+ html=HTML \
1600
+ http=HTTP \
1601
+ i18n=i18n \
1602
+ iam=IAM \
1603
+ ide=IDE \
1604
+ ini=INI \
1605
+ ios=iOS \
1606
+ ip=IP \
1607
+ jetbrains=JetBrains \
1608
+ js=JS \
1609
+ jsdoc=JSDoc \
1610
+ json=JSON \
1611
+ jwt=JWT \
1612
+ k8s=K8s \
1613
+ kubernetes=Kubernetes \
1614
+ l10n=l10n \
1615
+ less=Less \
1616
+ macos=macOS \
1617
+ mariadb=MariaDB \
1618
+ markdownlint=markdownlint \
1619
+ md=Markdown \
1620
+ mdx=MDX \
1621
+ mfa=MFA \
1622
+ mongodb=MongoDB \
1623
+ mysql=MySQL \
1624
+ nestjs=NestJS \
1625
+ nginx=Nginx \
1626
+ nodejs=Node.js \
1627
+ npm=npm \
1628
+ oauth=OAuth \
1629
+ oidc=OIDC \
1630
+ opcache=OPcache \
1631
+ orm=ORM \
1632
+ os=OS \
1633
+ pcov=PCOV \
1634
+ pdo=PDO \
1635
+ php-cs-fixer=PHP-CS-Fixer \
1636
+ php=PHP \
1637
+ phpat=PHPat \
1638
+ phpdoc=PHPDoc \
1639
+ phpstan=PHPStan \
1640
+ phpstorm=PhpStorm \
1641
+ phpunit=PHPUnit \
1642
+ pnpm=pnpm \
1643
+ postgres=PostgreSQL \
1644
+ psr=PSR \
1645
+ qa=QA \
1646
+ rabbitmq=RabbitMQ \
1647
+ rbac=RBAC \
1648
+ rest=REST \
1649
+ roadrunner=RoadRunner \
1650
+ rpc=RPC \
1651
+ saml=SAML \
1652
+ sass=Sass \
1653
+ scss=SCSS \
1654
+ sdk=SDK \
1655
+ seo=SEO \
1656
+ sql=SQL \
1657
+ sqlite=SQLite \
1658
+ ssh=SSH \
1659
+ ssl=SSL \
1660
+ sso=SSO \
1661
+ ssr=SSR \
1662
+ svg=SVG \
1663
+ tdd=TDD \
1664
+ tls=TLS \
1665
+ toml=TOML \
1666
+ ts=TS \
1667
+ twig-cs-fixer=Twig-CS-Fixer \
1668
+ typedoc=TypeDoc \
1669
+ typescript=TypeScript \
1670
+ ui=UI \
1671
+ ulid=ULID \
1672
+ uri=URI \
1673
+ url=URL \
1674
+ utf8=UTF-8 \
1675
+ uuid=UUID \
1676
+ ux=UX \
1677
+ wasm=WASM \
1678
+ webpack=webpack \
1679
+ xhprof=XHProf \
1680
+ xml=XML \
1681
+ yaml=YAML \
1682
+ yarn=Yarn \
1683
+ yml=YAML#vv #~~ scope names title casing gets wrong, as `<scope>=<name>` pairs, `_` a space
1684
+
1685
+ _CHANGELOG_LAST_TAG = $(shell $(GIT) describe --tags --abbrev=0 2>/dev/null)
1686
+ _IS_CHANGELOG_ALL = $(filter all a,$(_ARG_WORDS))
1687
+ _IS_CHANGELOG_WRITE = $(filter write w,$(_ARG_WORDS))
1688
+ _IS_CHANGELOG_NOTES = $(filter notes n,$(_ARG_WORDS))
1689
+ _CHANGELOG_FILE = $(CHANGELOG_DIR)/$(_CHANGELOG_MAJOR).x.md
1690
+ _CHANGELOG_MAJOR = $(firstword $(subst ., ,$(patsubst v%,%,$(_CHANGELOG_VERSION))))
1691
+ _CHANGELOG_RANGE = $(if $(_IS_CHANGELOG_ALL),,$(or $(CHANGELOG_RANGE),$(if $(_CHANGELOG_LAST_TAG),$(_CHANGELOG_LAST_TAG)..HEAD)))
1692
+ _CHANGELOG_PARTS = $(subst .., ,$(subst ...,..,$(_CHANGELOG_RANGE)))
1693
+ _CHANGELOG_START = $(word 1,$(_CHANGELOG_PARTS))
1694
+ _CHANGELOG_END = $(word 2,$(_CHANGELOG_PARTS))
1695
+ _CHANGELOG_VERSION = $(shell $(GIT) describe --tags --exact-match '$(_CHANGELOG_END)' 2>/dev/null || $(PRINTF) '%s' '$(VERSION)')
1696
+ _CHANGELOG_COMPARE = $(if $(and $(CHANGELOG_URL),$(_CHANGELOG_START)),$(CHANGELOG_URL)/compare/$(_CHANGELOG_START)...$(_CHANGELOG_END))
1697
+
1698
+ $(call _target,changelog): #~~ prints the changelog, or writes it to `CHANGELOG_DIR` #v
1699
+ $(DEBUG_PREFIX)$(call _capture,$(GIT) log --no-merges --pretty=format:'%H%x1f%D%x1f%s%x1f%b%x1e' $(_CHANGELOG_RANGE)) \
1700
+ | _CHANGELOG_URL="$(CHANGELOG_URL)" \
1701
+ _CHANGELOG_VERSION="$(_CHANGELOG_VERSION)" \
1702
+ _CHANGELOG_COMPARE="$(_CHANGELOG_COMPARE)" \
1703
+ _CHANGELOG_ALL="$(_IS_CHANGELOG_ALL)" \
1704
+ _CHANGELOG_COMMITS="$(CHANGELOG_SKIPPED_COMMITS)" \
1705
+ _CHANGELOG_TYPES="$(CHANGELOG_SKIPPED_TYPES)" \
1706
+ _CHANGELOG_MAJOR="$(if $(_IS_CHANGELOG_WRITE),$(_CHANGELOG_MAJOR))" \
1707
+ _CHANGELOG_GROUPS="$(CHANGELOG_GROUPS)" \
1708
+ _CHANGELOG_NAMES="$(CHANGELOG_NAMES)" \
1709
+ $(AWK) "$$_CHANGELOG_AWK" \
1710
+ | { $(if $(_IS_CHANGELOG_WRITE),$(call _changelog_write),$(if $(_IS_CHANGELOG_NOTES),$(call _changelog_notes),$(call _changelog_print))); }
1711
+
1712
+ #--- common
1713
+
1714
+ _FIXERS := $(foreach FIXER_NAME,$(_FIXER_NAMES),$(call _target,$(FIXER_NAME)))
1715
+ _DRY_RUNS := $(foreach FIXER_NAME,$(_FIXER_NAMES),$(call _target,$(FIXER_NAME)-dry-run))
1716
+ _ANALYZERS := $(foreach ANALYZER_NAME,$(_ANALYZER_NAMES),$(call _target,$(ANALYZER_NAME)))
1717
+ _TESTS := $(foreach TEST_NAME,$(_TEST_NAMES),$(call _target,$(TEST_NAME)))
1718
+ _UPDATES := $(foreach TEST_NAME,$(_TEST_NAMES),$(call _target,$(TEST_NAME)-update))
1719
+ _COVERAGE := $(foreach COVERAGE_NAME,$(_COVERAGE_NAMES),$(call _target,$(COVERAGE_NAME)-coverage))
1720
+ _GROUPS := $(foreach GROUP_NAME,$(_GROUP_NAMES),$(call _target,$(GROUP_NAME)-group))
1721
+ _PACKS := $(foreach PACK_NAME,$(_PACK_NAMES),$(call _target,$(PACK_NAME)))
1722
+
1723
+ CHECK_TARGETS ?= #v #~~ this project's own targets, run by `check`
1724
+
1725
+ $(call _target,check): #~~ runs the fixers without writing, and every analyzer
1726
+ $(TRACE_PREFIX)$(if $(_DRY_RUNS)$(_ANALYZERS)$(CHECK_TARGETS),:,$(call log,No tool to run.,$(COLOR_NOTICE)))
1727
+ $(DEBUG_PREFIX)$(if $(_DRY_RUNS)$(_ANALYZERS)$(CHECK_TARGETS),$(MAKE) $(_VERB_FLAGS) $(_DRY_RUNS) $(_ANALYZERS) $(CHECK_TARGETS),:)
1728
+
1729
+ _CI_TARGETS := $(call _target,check) \
1730
+ $(call _target,test)
1731
+
1732
+ CI_TARGETS ?= $(_CI_TARGETS)#vv #~~ targets `ci` runs in order
1733
+
1734
+ $(call _target,ci): #~~ runs `CI_TARGETS` one after the other #vv
1735
+ $(DEBUG_PREFIX)$(foreach TARGET,$(CI_TARGETS), \
1736
+ $(MAKE) $(_VERB_FLAGS) $(TARGET) && \
1737
+ ):
1738
+
1739
+ $(call _target,coverage): #~~ runs the tests and fails below the minimum coverage #vv
1740
+ $(TRACE_PREFIX)$(if $(_COVERAGE),:,$(call log,No test to run.,$(COLOR_NOTICE)))
1741
+ $(DEBUG_PREFIX)$(if $(_COVERAGE),$(MAKE) $(_VERB_FLAGS) $(_COVERAGE),:)
1742
+
1743
+ _FIX_ORDER := rector+php-cs-fixer \
1744
+ twig-cs-fixer \
1745
+ eslint+stylelint+markdownlint
1746
+
1747
+ _FIX_CHAINS := $(strip $(foreach CHAIN,$(_FIX_ORDER) $(filter-out $(subst +, ,$(_FIX_ORDER)),$(_FIXER_NAMES)), \
1748
+ $(if $(filter $(_FIXER_NAMES),$(subst +, ,$(CHAIN))),_$(_TARGET_PREFIX)fix-$(CHAIN)) \
1749
+ ))
1750
+
1751
+ FIX_TARGETS ?= #v #~~ this project's own targets, run by `fix`
1752
+
1753
+ $(call _target,fix): #~~ runs every fixer this project has, writing its fixes
1754
+ $(TRACE_PREFIX)$(if $(_FIX_CHAINS)$(FIX_TARGETS),:,$(call log,No fixer to run.,$(COLOR_NOTICE)))
1755
+ $(DEBUG_PREFIX)$(if $(_FIX_CHAINS)$(FIX_TARGETS),$(MAKE) $(_VERB_FLAGS) $(_FIX_CHAINS) $(FIX_TARGETS),:)
1756
+
1757
+ _$(_TARGET_PREFIX)fix-%:
1758
+ $(DEBUG_PREFIX)$(foreach NAME,$(filter $(_FIXER_NAMES),$(subst +, ,$*)), \
1759
+ $(MAKE) $(_VERB_FLAGS) $(call _target,$(NAME)) && \
1760
+ ):
1761
+
1762
+ GROUP_TARGETS ?= #v #~~ this project's own targets, run by `group`
1763
+
1764
+ $(call _target,group): #~~ counts every tool's findings by identifier #v
1765
+ $(TRACE_PREFIX)$(if $(_GROUPS)$(GROUP_TARGETS),:,$(call log,No tool to run.,$(COLOR_NOTICE)))
1766
+ $(DEBUG_PREFIX)$(if $(_GROUPS)$(GROUP_TARGETS),$(MAKE) $(_VERB_FLAGS) $(_GROUPS) $(GROUP_TARGETS),:)
1767
+
1768
+ $(call _target,pack): $(_PACKS) #~~ packs every stack's package into `./.local` #vvv
1769
+ $(TRACE_PREFIX)$(if $(_PACKS),:,$(call log,No package to pack.,$(COLOR_NOTICE)))
1770
+
1771
+ TEST_TARGETS ?= #v #~~ this project's own targets, run by `test`
1772
+
1773
+ $(call _target,test): #~~ runs the tests of every stack this project has
1774
+ $(TRACE_PREFIX)$(if $(_TESTS)$(TEST_TARGETS),:,$(call log,No test to run.,$(COLOR_NOTICE)))
1775
+ $(DEBUG_PREFIX)$(if $(_TESTS)$(TEST_TARGETS),$(MAKE) $(_VERB_FLAGS) $(_TESTS) $(TEST_TARGETS),:)
1776
+
1777
+ $(call _target,test-update): #~~ runs those tests and updates their snapshots #v
1778
+ $(TRACE_PREFIX)$(if $(_UPDATES),:,$(call log,No test to run.,$(COLOR_NOTICE)))
1779
+ $(DEBUG_PREFIX)$(if $(_UPDATES),$(MAKE) $(_VERB_FLAGS) $(_UPDATES),:)
1780
+
1781
+ FIXTURE_TARGETS ?= #v #~~ this project's own targets, run by `fixtures` to build what its tests read
1782
+
1783
+ $(call _target,fixtures): #~~ builds the fixtures this project's tests read #v
1784
+ $(TRACE_PREFIX)$(if $(FIXTURE_TARGETS),:,$(call log,No fixture to build.,$(COLOR_NOTICE)))
1785
+ $(DEBUG_PREFIX)$(if $(FIXTURE_TARGETS),$(MAKE) $(_VERB_FLAGS) --no-keep-going $(FIXTURE_TARGETS),:)
1786
+
1787
+ .DEFAULT:
1788
+ ifeq ($(TARGET),) # unknown
1789
+ $(DEBUG_PREFIX)$(if $(_ALIAS_GOAL),exec $(MAKE) $(_MAKE_FLAGS) $(_ALIAS_GOAL) \
1790
+ $(if $(_UNKNOWN_ARGS),-- $(_UNKNOWN_ARGS)),:)
1791
+ $(DEBUG_PREFIX)$(if $(_ALIAS_TARGET),$(if $(_ALIAS_GOAL),:,{ \
1792
+ $(call log,`%s` aliases `%s`$(_COMMA) which is not a target.,$(COLOR_ERROR),'$@' '$(_ALIAS_TARGET)'); \
1793
+ exit $(BRNSHKR_CONFIG_ERROR_CODE); \
1794
+ }),:)
1795
+ $(DEBUG_PREFIX)$(if $(_ALIAS_TARGET),:,$(if $(_IS_FIRST_GOAL),$(call log,Unknown command `%s`.,$(COLOR_ERROR),'$@'),:))
1796
+ $(DEBUG_PREFIX)$(if $(_ALIAS_TARGET),:,$(if $(_IS_FIRST_GOAL),$(if $(_IS_JUST_PRINT),:,$(call _suggest)),:))
1797
+ else
1798
+ $(DEBUG_PREFIX):
1799
+ endif # /unknown
1800
+
1801
+ _PREFLIGHT := _$(_TARGET_PREFIX)assert-no-collisions
1802
+
1803
+ $(filter-out $(_PREFLIGHT),$(_ALL_TARGETS)): | $(_PREFLIGHT)
1804
+
1805
+ _$(_TARGET_PREFIX)assert-no-collisions:
1806
+ ifneq ($(_DUPLICATE_TARGETS),) # duplicates
1807
+ $(TRACE_PREFIX)$(foreach NAME,$(_DUPLICATE_TARGETS),$(call log,`%s` is defined in %s.,$(COLOR_ERROR),'$(NAME)' '$(call _join_as_quoted_list,$(call _named_path,$(call _definitions_of,$(NAME))))')$(_NEWLINE))
1808
+ $(TRACE_PREFIX)$(call log,Rename $(if $(word 2,$(_DUPLICATE_TARGETS)),them,it)$(_COMMA) since this project defines the same name twice.,$(COLOR_NOTICE))
1809
+ $(TRACE_PREFIX)exit $(BRNSHKR_CONFIG_ERROR_CODE)
1810
+ endif # /duplicates
1811
+ ifneq ($(and $(strip $(TARGET_PREFIX)),$(_COLLIDING_TARGETS)),) # collisions
1812
+ $(TRACE_PREFIX)$(foreach NAME,$(_COLLIDING_TARGETS),$(call log,`%s` is defined in %s.,$(COLOR_ERROR),'$(_TARGET_PREFIX)$(NAME)' '$(call _join_as_quoted_list,$(call _named_path,$(call _definitions_of,$(_TARGET_PREFIX)$(NAME))))')$(_NEWLINE))
1813
+ $(TRACE_PREFIX)$(call log,Rename $(if $(word 2,$(_COLLIDING_TARGETS)),them,it)$(_COMMA) since `TARGET_PREFIX` is already the namespace this project chose.,$(COLOR_NOTICE))
1814
+ $(TRACE_PREFIX)exit $(BRNSHKR_CONFIG_ERROR_CODE)
1815
+ endif # /collisions
1816
+
1817
+ PHONY ?= 1#v #~~ targets marked `.PHONY`, `shared` for the shared ones only or `0` for none
1818
+
1819
+ .PHONY: $(if $(filter-out $(_FALSE),$(PHONY)),$(if $(filter shared,$(PHONY)),$(_SHARED_TARGETS),$(_ALL_TARGETS)))
1820
+
1821
+ #--- ansi
1822
+
1823
+ _ANSI_MODIFIER_NORMAL := 0
1824
+ _ANSI_MODIFIER_BOLD := 1
1825
+ _ANSI_MODIFIER_UNDERLINE := 4
1826
+ _ANSI_MODIFIER_REVERSE := 7
1827
+
1828
+ _ANSI_COLOR_NORMAL := 0
1829
+ _ANSI_COLOR_BLACK := 30
1830
+ _ANSI_COLOR_RED := 31
1831
+ _ANSI_COLOR_GREEN := 32
1832
+ _ANSI_COLOR_YELLOW := 33
1833
+ _ANSI_COLOR_BLUE := 34
1834
+ _ANSI_COLOR_MAGENTA := 35
1835
+ _ANSI_COLOR_CYAN := 36
1836
+ _ANSI_COLOR_WHITE := 37
1837
+
1838
+ _ANSI_COLOR_BRIGHT_NORMAL := 0
1839
+ _ANSI_COLOR_BRIGHT_BLACK := 90
1840
+ _ANSI_COLOR_BRIGHT_RED := 91
1841
+ _ANSI_COLOR_BRIGHT_GREEN := 92
1842
+ _ANSI_COLOR_BRIGHT_YELLOW := 93
1843
+ _ANSI_COLOR_BRIGHT_BLUE := 94
1844
+ _ANSI_COLOR_BRIGHT_MAGENTA := 95
1845
+ _ANSI_COLOR_BRIGHT_CYAN := 96
1846
+ _ANSI_COLOR_BRIGHT_WHITE := 97
1847
+
1848
+ _ANSI_RESET = $(if $(_IS_NO_ANSI),,\033[0m)
1849
+ _ANSI_CLEAR_LINE = $(if $(_IS_NO_ANSI),,\033[2K)
1850
+ _ANSI_HIDE_CURSOR = $(if $(_IS_NO_ANSI),,\033[?25l)
1851
+ _ANSI_SHOW_CURSOR = $(if $(_IS_NO_ANSI),,\033[?25h)
1852
+
1853
+ _UPPER_normal := NORMAL
1854
+ _UPPER_black := BLACK
1855
+ _UPPER_red := RED
1856
+ _UPPER_green := GREEN
1857
+ _UPPER_yellow := YELLOW
1858
+ _UPPER_blue := BLUE
1859
+ _UPPER_magenta := MAGENTA
1860
+ _UPPER_cyan := CYAN
1861
+ _UPPER_white := WHITE
1862
+
1863
+ #---v helpers
1864
+
1865
+ #**
1866
+ #* Wraps a string in ANSI color and modifier escapes, resetting after it.
1867
+ #* The color and the modifiers may be given together or apart.
1868
+ #*
1869
+ #* parameters:
1870
+ #* text: string
1871
+ #* color_and_or_modifiers?: AnyOf<normal | white | black | red | yellow | green | cyan | blue | magenta | bold | underline | reverse | bright>
1872
+ #* modifiers?: AnyOf<normal | bold | underline | reverse | bright>
1873
+ #*
1874
+ #* returns: string
1875
+ #*
1876
+ define text
1877
+ $(call _ansi,$2 $3)$1$(_ANSI_RESET)
1878
+ endef
1879
+
1880
+ #**
1881
+ #* The path on this machine of a file the tools see under `WORKDIR`.
1882
+ #*
1883
+ #* parameters:
1884
+ #* path: string
1885
+ #*
1886
+ #* returns: string
1887
+ #*
1888
+ define _host_path
1889
+ $(patsubst $(WORKDIR)/%,$(CURDIR)/%,$1)
1890
+ endef
1891
+
1892
+ #**
1893
+ #* Turns a list of extensions into the brace glob a tool is handed.
1894
+ #*
1895
+ #* parameters:
1896
+ #* extensions: list<string>
1897
+ #*
1898
+ #* returns: string
1899
+ #*
1900
+ define _brace_glob
1901
+ **/*.{$(subst $(_SPACE),$(_COMMA),$(strip $1))}
1902
+ endef
1903
+
1904
+ #**
1905
+ #* Whether `--dry-run` is among the given words.
1906
+ #* The Makefile's own pseudo-flag, since none of these fixers understand it natively; its presence
1907
+ #* tells the plain target to skip `--fix`.
1908
+ #*
1909
+ #* parameters:
1910
+ #* words: list<string>
1911
+ #*
1912
+ #* returns: string
1913
+ #*
1914
+ define _is_dry_run
1915
+ $(filter --dry-run,$1)
1916
+ endef
1917
+
1918
+ #**
1919
+ #* The given words with the Makefile's own `--dry-run` pseudo-flag removed, so it never reaches the tool.
1920
+ #*
1921
+ #* parameters:
1922
+ #* words: list<string>
1923
+ #*
1924
+ #* returns: list<string>
1925
+ #*
1926
+ define _without_dry_run
1927
+ $(filter-out --dry-run,$1)
1928
+ endef
1929
+
1930
+ #**
1931
+ #* Whether the given words contain a real positional argument.
1932
+ #* Not a flag, and not the value a preceding flag from the second list consumes.
1933
+ #*
1934
+ #* parameters:
1935
+ #* words: list<string>
1936
+ #* value_flags: list<string>
1937
+ #*
1938
+ #* returns: string
1939
+ #*
1940
+ define _has_positional_arg
1941
+ $(strip $(shell $(PRINTF) '%s\n' $1 | $(AWK) -v _VALUE_FLAGS=' $(strip $2) ' '\
1942
+ { \
1943
+ if (skip) { skip = 0; next } \
1944
+ if (index(_VALUE_FLAGS, " " $$0 " ") > 0) { skip = 1; next } \
1945
+ if ($$0 != "" && substr($$0, 1, 1) != "-") { print 1; exit } \
1946
+ }'))
1947
+ endef
1948
+
1949
+ #**
1950
+ #* The name a config answers to, which is its file name without a leading dot, a `.dist` or a suffix.
1951
+ #*
1952
+ #* parameters:
1953
+ #* path: string
1954
+ #*
1955
+ #* returns: string
1956
+ #*
1957
+ define _config_name
1958
+ $(firstword $(subst ., ,$(patsubst .%,%,$(notdir $(lastword $(subst |, ,$1))))))
1959
+ endef
1960
+
1961
+ #**
1962
+ #* The configs a run writes, which is every one of them unless the run named some.
1963
+ #*
1964
+ #* parameters:
1965
+ #* configs: list<string>
1966
+ #*
1967
+ #* returns: list<string>
1968
+ #*
1969
+ define _selected_configs
1970
+ $(if $(_NAMED_CONFIGS),$(foreach CONFIG,$1,$(if $(filter $(call _config_name,$(CONFIG)),$(_NAMED_CONFIGS)),$(CONFIG))),$1)
1971
+ endef
1972
+
1973
+ #**
1974
+ #* The tracked half of a config path, which is the path itself once it names the `.dist` file.
1975
+ #*
1976
+ #* parameters:
1977
+ #* path: string
1978
+ #*
1979
+ #* returns: string
1980
+ #*
1981
+ define _tracked
1982
+ $(if $(findstring .dist.,$1),$1,$(basename $1).dist$(suffix $1))
1983
+ endef
1984
+
1985
+ #**
1986
+ #* The private half beside wherever the tool resolved its config.
1987
+ #* Empty where it resolved something outside that pair — an `eslint.ts` rather than either half of
1988
+ #* the `.mjs` one.
1989
+ #*
1990
+ #* parameters:
1991
+ #* resolved: string
1992
+ #* extension: string
1993
+ #*
1994
+ #* returns: string
1995
+ #*
1996
+ define _layered
1997
+ $(if $(filter %.$2 %.dist.$2,$1),$(subst .dist.,.,$1))
1998
+ endef
1999
+
2000
+ #**
2001
+ #* The same paths, rooted in the project's own `conf/`.
2002
+ #* Anything the config search resolved inside an installation of this package moves back, so a write
2003
+ #* never lands in `vendor/` or `node_modules/`.
2004
+ #*
2005
+ #* parameters:
2006
+ #* paths: list<string>
2007
+ #*
2008
+ #* returns: list<string>
2009
+ #*
2010
+ define _in_project
2011
+ $(foreach CONFIG,$1,$(if $(filter $(addsuffix /%,$(_PACKAGE_CONF_DIRS)),$(CONFIG)),$(WORKDIR)/conf/$(notdir $(CONFIG)),$(CONFIG)))
2012
+ endef
2013
+
2014
+ #**
2015
+ #* Names the project's root namespace in a copied PHP config's `@internal` tag.
2016
+ #* A file with no namespace still says where it belongs. Falls back to `App`, and does nothing
2017
+ #* without an autoloader.
2018
+ #*
2019
+ #* parameters:
2020
+ #* path: string
2021
+ #*
2022
+ #* returns: void
2023
+ #*
2024
+ define _name_internal_tag
2025
+ if [ -f "$(call _host_path,$(_AUTOLOADER))" ]; then \
2026
+ $(PHP) -r "$$_NAME_INTERNAL_TAG_PROGRAM" "$(_AUTOLOADER)" "$(WORKDIR)/$1" \
2027
+ || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
2028
+ fi
2029
+ endef
2030
+
2031
+ #**
2032
+ #* A list in reverse order.
2033
+ #*
2034
+ #* parameters:
2035
+ #* items: list<string>
2036
+ #*
2037
+ #* returns: list<string>
2038
+ #*
2039
+ define _reverse
2040
+ $(if $1,$(call _reverse,$(wordlist 2,$(words $1),$1)) $(firstword $1))
2041
+ endef
2042
+
2043
+ #**
2044
+ #* The directories a config is looked for in, most specific first.
2045
+ #* `$(CURDIR)` is deliberately absent: a bare config at the repository root would re-enter PHPStan's
2046
+ #* dynamically evaluated paths.
2047
+ #*
2048
+ #* parameters:
2049
+ #* stack: string
2050
+ #*
2051
+ #* returns: list<string>
2052
+ #*
2053
+ define _config_dirs
2054
+ $(foreach BASE,$(call _reverse,$(_SEARCH_BASES)),$(if $1,$(WORKDIR)/$(BASE)/$1) $(WORKDIR)/$(BASE))
2055
+ endef
2056
+
2057
+ #**
2058
+ #* The candidate names this package ships, which is the tracked half alone.
2059
+ #*
2060
+ #* parameters:
2061
+ #* candidates: list<string>
2062
+ #*
2063
+ #* returns: list<string>
2064
+ #*
2065
+ define _shipped_names
2066
+ $(foreach NAME,$(notdir $1),$(if $(findstring .dist.,$(NAME)),$(NAME)))
2067
+ endef
2068
+
2069
+ #**
2070
+ #* The configs a directory has, in the order the names were given.
2071
+ #*
2072
+ #* parameters:
2073
+ #* directory: string
2074
+ #* names: list<string>
2075
+ #*
2076
+ #* returns: list<string>
2077
+ #*
2078
+ define _configs_in
2079
+ $(foreach NAME,$2,$(if $(wildcard $(call _host_path,$1/$(NAME))),$1/$(NAME)))
2080
+ endef
2081
+
2082
+ #**
2083
+ #* The first config in the list that is there.
2084
+ #* Then the tracked name under either installation of this package, so a project reads its own file,
2085
+ #* falls back to the tracked one, and may keep neither. The last candidate is what a project is told
2086
+ #* to create when it has none, and `CONFIG` pins the choice.
2087
+ #*
2088
+ #* parameters:
2089
+ #* candidates: list<string>
2090
+ #* stack: string
2091
+ #*
2092
+ #* returns: string
2093
+ #*
2094
+ define _config
2095
+ $(if $(_PINNED_CONFIG),$(if $(filter local,$(_PINNED_CONFIG)),$(firstword $1),$(lastword $1)),$(or \
2096
+ $(firstword $(foreach DIRECTORY,$(call _config_dirs,$2),$(call _configs_in,$(DIRECTORY),$(notdir $1))) \
2097
+ $(foreach DIRECTORY,$(_PACKAGE_CONF_DIRS),$(call _configs_in,$(DIRECTORY),$(call _shipped_names,$1)))),$(lastword $1)))
2098
+ endef
2099
+
2100
+ #**
2101
+ #* The path a message names it by: relative to the checkout, the way the reader would type it.
2102
+ #*
2103
+ #* parameters:
2104
+ #* path: string
2105
+ #*
2106
+ #* returns: string
2107
+ #*
2108
+ define _named_path
2109
+ $(patsubst $(CURDIR)/%,./%,$(call _host_path,$1))
2110
+ endef
2111
+
2112
+ #**
2113
+ #* Offers the closest targets to what was typed, asking before running the only one where there is one.
2114
+ #*
2115
+ #* parameters:
2116
+ #* prefix?: string
2117
+ #*
2118
+ #* returns: void
2119
+ #*
2120
+ define _suggest
2121
+ $(if $(_SUGGESTED_TARGETS),$(if $(word 2,$(_SUGGESTED_TARGETS)), \
2122
+ $(call log,Did you mean %s?,$(COLOR_NOTICE),'$(call _join_as_quoted_list,$(addprefix $1,$(_SUGGESTED_TARGETS)),disjunction)'); \
2123
+ exit $(BRNSHKR_CONFIG_ERROR_CODE),if [ -t 0 ] && [ -z "$(_IS_CI)" ]; then \
2124
+ $(call confirm,Did you mean `$1$(_SUGGESTED_TARGETS)`?) || exit $(BRNSHKR_CONFIG_ERROR_CODE); \
2125
+ exec $(_DOTENV_RESET)$(MAKE) $(_MAKE_FLAGS) $(_INHERITED_FLAGS) $1$(_SUGGESTED_TARGETS) \
2126
+ $(if $(_UNKNOWN_ARGS),-- $(_UNKNOWN_ARGS)); \
2127
+ fi; \
2128
+ $(call log,Did you mean `%s`?,$(COLOR_NOTICE),'$1$(_SUGGESTED_TARGETS)'); \
2129
+ exit $(BRNSHKR_CONFIG_ERROR_CODE)),$(call log,Run `make $(call _target,help)` or just `make` to see available commands.,$(COLOR_NOTICE)); \
2130
+ exit $(BRNSHKR_CONFIG_ERROR_CODE))
2131
+ endef
2132
+
2133
+ #**
2134
+ #* Fails the recipe when an argument is not one of the values the target accepts.
2135
+ #*
2136
+ #* parameters:
2137
+ #* noun: string
2138
+ #*
2139
+ #* returns: void
2140
+ #*
2141
+ define _require_known_value
2142
+ $(if $(_UNKNOWN_VALUES),{ \
2143
+ $(call log,No $1 named `%s`.,$(COLOR_ERROR),'$(_MISTYPED_VALUE)'); \
2144
+ $(if $(_SUGGESTED_VALUES),$(if $(word 2,$(_SUGGESTED_VALUES)), \
2145
+ $(call log,Did you mean %s?,$(COLOR_NOTICE),'$(call _join_as_quoted_list,$(_SUGGESTED_VALUES),disjunction)'), \
2146
+ $(call log,Did you mean `%s`?,$(COLOR_NOTICE),'$(_SUGGESTED_VALUES)')), \
2147
+ $(call log,Try %s.,$(COLOR_NOTICE),'$(call _join_as_quoted_list,$(_ARG_VALUES),disjunction)',no-autolink)); \
2148
+ exit $(BRNSHKR_CONFIG_ERROR_CODE); \
2149
+ },:)
2150
+ endef
2151
+
2152
+ #**
2153
+ #* Writes each `GIT_HOOKS` pair into this checkout's git hooks, keeping what is there.
2154
+ #*
2155
+ #* parameters:
2156
+ #* replace?: OneOf<yes | no> = no
2157
+ #*
2158
+ #* returns: void
2159
+ #*
2160
+ define _install_git_hooks
2161
+ $(GIT) rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0; \
2162
+ directory="$$($(GIT) rev-parse --git-path hooks)"; \
2163
+ $(MKDIR) -p "$$directory"; \
2164
+ for pair in $(GIT_HOOKS); do \
2165
+ hook="$${pair%%=*}"; \
2166
+ $(if $(filter yes,$1),,[ ! -f "$$directory/$$hook" ] || continue;) \
2167
+ $(PRINTF) '#!/bin/sh\nexec %s %s\n' '$(MAKE)' "$${pair#*=}" > "$$directory/$$hook"; \
2168
+ $(CHMOD) +x "$$directory/$$hook"; \
2169
+ $(call log,Installed `%s`.,$(COLOR_SUCCESS),"$$hook"); \
2170
+ done
2171
+ endef
2172
+
2173
+ #**
2174
+ #* Fails the recipe when a configured file is not there, naming the variable it came from.
2175
+ #*
2176
+ #* parameters:
2177
+ #* path: string
2178
+ #* variable: string
2179
+ #*
2180
+ #* returns: void
2181
+ #*
2182
+ define _require_file
2183
+ test -f '$(call _host_path,$1)' || { \
2184
+ $(call log,`%s` is missing$(_COMMA) set `%s` or create it.,$(COLOR_ERROR),'$(call _named_path,$1)' '$2') >&2; \
2185
+ exit $(BRNSHKR_CONFIG_ERROR_CODE); \
2186
+ }
2187
+ endef
2188
+
2189
+ _IS_GROUP_PARALLEL = $(if $(filter -O%,$(MAKEFLAGS)),,$(_IS_PARALLEL))
2190
+ _HAS_GROUP_LABEL = $(and $(_IS_ANNOUNCING),$(ANNOUNCEMENT))
2191
+ _CLEAR_SPINNER = $(PRINTF) "\r$(_ANSI_CLEAR_LINE)$(_ANSI_SHOW_CURSOR)"
2192
+ _SPINNER_SECONDS := 0.15
2193
+ _SIGNAL_INTERRUPT := INT
2194
+ _SIGNAL_TERMINATE := TERM
2195
+ _SIGNAL_HANGUP := HUP
2196
+ _INTERRUPT_EXIT_CODE := 130
2197
+ _SPINNER_SIGNALS := $(_SIGNAL_INTERRUPT) $(_SIGNAL_TERMINATE) $(_SIGNAL_HANGUP)
2198
+
2199
+ #**
2200
+ #* Starts a spinner in the background, which runs until the shell that started it is gone.
2201
+ #*
2202
+ #* parameters:
2203
+ #* indent?: string
2204
+ #*
2205
+ #* returns: void
2206
+ #*
2207
+ define __spinner
2208
+ while kill -0 $$$$ 2>/dev/null; do for frame in '|' '/' '-' '\'; do \
2209
+ $(PRINTF) '\r$1$(call text,%s Running…,$(COLOR_DESCRIPTION))' "$$frame"; \
2210
+ sleep $(_SPINNER_SECONDS) 2>/dev/null || sleep 1; \
2211
+ done; done & spinner=$$!
2212
+ endef
2213
+
2214
+ #**
2215
+ #* Runs a tool and prints its findings grouped by identifier, failing only when it reported nothing.
2216
+ #*
2217
+ #* parameters:
2218
+ #* command: string
2219
+ #* identifier: string
2220
+ #* message?: string
2221
+ #*
2222
+ #* returns: void
2223
+ #*
2224
+ define _group
2225
+ label=$(if $(_HAS_GROUP_LABEL),$$($(call log,$(ANNOUNCEMENT),$(COLOR_NOTICE),'$@'))); \
2226
+ has_spinner=; $(if $(or $(_IS_GROUP_PARALLEL),$(_IS_NO_ANSI)),,[ -t 1 ] && has_spinner=1;) \
2227
+ $(if $(_IS_GROUP_PARALLEL),,[ -z "$$label" ] || $(PRINTF) '%s\n' "$$label"; \
2228
+ [ -z "$$has_spinner" ] || { \
2229
+ $(PRINTF) '$(_ANSI_HIDE_CURSOR)'; \
2230
+ trap 'kill $$spinner 2>/dev/null; $(_CLEAR_SPINNER); exit $(_INTERRUPT_EXIT_CODE)' $(_SPINNER_SIGNALS); \
2231
+ $(call __spinner,$(if $(_HAS_GROUP_LABEL), )); \
2232
+ };) \
2233
+ output=$$($1 2>&1); status=$$?; \
2234
+ report=$$($(PRINTF) '%s\n' "$$output" \
2235
+ | $(AWK) -v _INDENT='$(if $(_HAS_GROUP_LABEL),$(INDENT),0)' -v _IDENTIFIER='$2' -v _MESSAGE='$3' -v _STATUS="$$status" "$$_GROUP_AWK"); code=$$?; \
2236
+ $(if $(_IS_GROUP_PARALLEL),[ -z "$$report" ] || $(PRINTF) '%s\n' $${label:+"$$label"} "$$report";, \
2237
+ [ -z "$$has_spinner" ] || { \
2238
+ kill $$spinner 2>/dev/null; \
2239
+ wait $$spinner 2>/dev/null; \
2240
+ $(_CLEAR_SPINNER); \
2241
+ }; \
2242
+ [ -z "$$report" ] || $(PRINTF) '%s\n' "$$report";) \
2243
+ exit $$code
2244
+ endef
2245
+
2246
+ #**
2247
+ #* Runs a tool behind a spinner, which clears as soon as the tool prints anything.
2248
+ #*
2249
+ #* parameters:
2250
+ #* command: string
2251
+ #*
2252
+ #* returns: void
2253
+ #*
2254
+ define _spinner
2255
+ spin_command() { $1; }; \
2256
+ has_spinner=; $(if $(or $(_IS_PARALLEL),$(_IS_NO_ANSI)),,[ -t 1 ] \
2257
+ && [ -n '$(SCRIPT)' ] \
2258
+ && command -v $(SCRIPT) >/dev/null 2>&1 \
2259
+ && has_spinner=1;) \
2260
+ if [ -z "$$has_spinner" ]; then \
2261
+ spin_command; \
2262
+ else \
2263
+ export _SPIN_STATUS=$$($(MKTEMP)); \
2264
+ script_command='($(subst $(_QUOTE),$(_QUOTE)\$(_QUOTE)$(_QUOTE),$1)); $(PRINTF) "%s" $$? > "$$_SPIN_STATUS"'; \
2265
+ if $(SCRIPT) --version >/dev/null 2>&1; then \
2266
+ set -- -qfc "$$script_command" /dev/null; \
2267
+ else \
2268
+ set -- -q /dev/null $(SHELL) -c "$$script_command"; \
2269
+ fi; \
2270
+ $(PRINTF) '$(_ANSI_HIDE_CURSOR)'; \
2271
+ trap '$(_CLEAR_SPINNER); exit $(_INTERRUPT_EXIT_CODE)' $(_SPINNER_SIGNALS); \
2272
+ $(call __spinner); \
2273
+ SHELL='$(SHELL)' $(SCRIPT) "$$@" | { \
2274
+ IFS= read -r line; has_line=$$?; \
2275
+ kill $$spinner 2>/dev/null; \
2276
+ wait $$spinner 2>/dev/null; \
2277
+ $(_CLEAR_SPINNER); \
2278
+ [ $$has_line -ne 0 ] || $(PRINTF) '%s\n' "$$line"; \
2279
+ $(CAT); \
2280
+ }; \
2281
+ code=$$($(CAT) "$$_SPIN_STATUS"); \
2282
+ $(RM) -f "$$_SPIN_STATUS"; \
2283
+ [ -n "$$code" ] || { spin_command; exit $$?; }; \
2284
+ exit $$code; \
2285
+ fi
2286
+ endef
2287
+
2288
+ #**
2289
+ #* Prints a command's output for a pipe to read, failing the recipe when the command itself failed.
2290
+ #*
2291
+ #* parameters:
2292
+ #* command: string
2293
+ #*
2294
+ #* returns: void
2295
+ #*
2296
+ define _capture
2297
+ output=$$($1) || { $(PRINTF) '%s\n' "$$output" >&2; exit $(BRNSHKR_CONFIG_ERROR_CODE); }; $(PRINTF) '%s\n' "$$output"
2298
+ endef
2299
+
2300
+ #**
2301
+ #* Fails the recipe when one metric of a text coverage report falls short, as every runner but Pest needs.
2302
+ #*
2303
+ #* parameters:
2304
+ #* path: string
2305
+ #* metric: string
2306
+ #* minimum: number
2307
+ #* variable: string
2308
+ #*
2309
+ #* returns: void
2310
+ #*
2311
+ define _require_coverage
2312
+ $(TRACE_PREFIX)$(call _require_file,$1,$4)
2313
+ $(TRACE_PREFIX)$(AWK) -v _METRIC='$2' -v _MINIMUM='$3' '$$0 ~ _METRIC && !found { if (match($$0, /[0-9]+(\.[0-9]+)?%/)) { reached = substr($$0, RSTART, RLENGTH - 1) + 0; found = 1 } } END { exit reached < _MINIMUM + 0 }' '$1' || { \
2314
+ $(call log,`%s` coverage is below `%s%%`.,$(COLOR_ERROR),'$2' '$3') >&2; \
2315
+ exit $(BRNSHKR_CONFIG_ERROR_CODE); \
2316
+ }
2317
+ endef
2318
+
2319
+ #**
2320
+ #* A list of words, each quoted so the shell reads it as one argument.
2321
+ #*
2322
+ #* parameters:
2323
+ #* words: list<string>
2324
+ #*
2325
+ #* returns: list<string>
2326
+ #*
2327
+ define _shell_quoted
2328
+ $(foreach WORD,$1,'$(subst $(_QUOTE),$(_QUOTE)\$(_QUOTE)$(_QUOTE),$(WORD))')
2329
+ endef
2330
+
2331
+ #**
2332
+ #* Joins a list the way `Str::joinAsQuotedList()` and its JavaScript twin do, so the three read alike.
2333
+ #*
2334
+ #* parameters:
2335
+ #* items: list<string>
2336
+ #* type?: OneOf<conjunction | disjunction> = conjunction
2337
+ #*
2338
+ #* returns: string
2339
+ #*
2340
+ define _join_as_quoted_list
2341
+ $(if $(word 2,$1),$(subst $(_SPACE),$(_COMMA)$(_SPACE),$(foreach \
2342
+ ITEM,$(filter-out $(lastword $1),$1),`$(ITEM)`)) $(if \
2343
+ $(filter disjunction,$2),or,and) `$(lastword $1)`,$(if \
2344
+ $1,`$1`))
2345
+ endef
2346
+
2347
+ #**
2348
+ #* Asks a yes/no question before something destructive happens.
2349
+ #* It answers itself where nobody can: under `CI`, or without a terminal to ask at, it answers the default.
2350
+ #* It succeeds on yes, so the caller reads it as a condition and says what it skipped.
2351
+ #*
2352
+ #* parameters:
2353
+ #* question: string
2354
+ #* default?: OneOf<yes | no> = no
2355
+ #*
2356
+ #* returns: void
2357
+ #*
2358
+ define confirm
2359
+ { \
2360
+ if [ -n "$(_IS_CI)" ] || ! ( : < /dev/tty ) 2>/dev/null; then \
2361
+ $(if $(filter yes,$2),true,false); \
2362
+ else \
2363
+ $(PRINTF) '%s $(call text,[$(if $(filter yes,$2),Y/n,y/N)],$(COLOR_NOTICE)) ' '$1' \
2364
+ | $(call _render_inline_code,printf "%s"$(_COMMA) $$0,yes) >&2 \
2365
+ && trap '$(PRINTF) "\n" >&2; trap - $(_SIGNAL_INTERRUPT); kill -$(_SIGNAL_INTERRUPT) $$$$' $(_SIGNAL_INTERRUPT) \
2366
+ && read -r answer < /dev/tty \
2367
+ && trap - $(_SIGNAL_INTERRUPT) \
2368
+ && case "$$answer" in \
2369
+ [yY]|[yY][eE][sS]) true;; \
2370
+ "") $(if $(filter yes,$2),true,false);; \
2371
+ *) false;; \
2372
+ esac; \
2373
+ fi; \
2374
+ }
2375
+ endef
2376
+
2377
+ #**
2378
+ #* Prints a message behind the `<VENDOR>/<PACKAGE>` label.
2379
+ #* Anything in backticks renders as inline code. The message is a `printf` format, so a value goes
2380
+ #* in as an argument rather than being pasted in. A literal comma has to be written as `$(_COMMA)`.
2381
+ #* A backticked span that names a target or an existing path links to it, which `no-autolink` turns
2382
+ #* off where the sentence is about neither.
2383
+ #*
2384
+ #* parameters:
2385
+ #* message: string
2386
+ #* color_and_or_modifiers?: AnyOf<normal | white | black | red | yellow | green | cyan | blue | magenta | bold | underline | reverse | bright>
2387
+ #* arguments?: list<string>
2388
+ #* autolink?: OneOf<autolink | no-autolink> = autolink
2389
+ #*
2390
+ #* returns: void
2391
+ #*
2392
+ define log
2393
+ $(PRINTF) '$(call text,[$(_LABEL)],$2) $1\n' $3 \
2394
+ | $(call _render_inline_code,print,$(if $(filter no-autolink,$4),,yes))
2395
+ endef
2396
+
2397
+ #**
2398
+ #* Renders every backticked span of a line as inline code, ending with the given awk action.
2399
+ #* A span links to the target or the path it names only where the caller asks for it.
2400
+ #*
2401
+ #* parameters:
2402
+ #* action: string
2403
+ #* autolink?: OneOf<yes | no> = no
2404
+ #*
2405
+ #* returns: void
2406
+ #*
2407
+ define _render_inline_code
2408
+ $(AWK) -v _OPEN='$(call _ansi,$(COLOR_HIGHLIGHT))' \
2409
+ -v _CLOSE='$(_ANSI_RESET)' \
2410
+ -v _URL='$(if $(filter yes,$2),$(_INLINE_CODE_URL))' \
2411
+ -v _CWD='$(CURDIR)' \
2412
+ -v _PREFIX='$(_TARGET_PREFIX)' \
2413
+ -v _STAGES=' $(_DOTENV_STAGES) ' \
2414
+ -v _TARGETS=' $(if $(filter yes,$2),$(_TARGET_LINKS)) ' ' \
2415
+ function render_url(url, path, line, placeholder_position) { \
2416
+ placeholder_position = index(url, "{file}"); \
2417
+ url = substr(url, 1, placeholder_position - 1) path substr(url, placeholder_position + 6); \
2418
+ placeholder_position = index(url, "{line}"); \
2419
+ if (placeholder_position) url = substr(url, 1, placeholder_position - 1) line substr(url, placeholder_position + 6); \
2420
+ return url \
2421
+ } \
2422
+ function target_definition(name, key, position, definition, dash) { \
2423
+ key = name; \
2424
+ position = index(_TARGETS, " " key "="); \
2425
+ if (!position && _PREFIX != "" && index(key, _PREFIX) == 1) { \
2426
+ key = substr(key, length(_PREFIX) + 1); \
2427
+ position = index(_TARGETS, " " key "=") \
2428
+ } \
2429
+ dash = index(key, "-"); \
2430
+ if (!position && dash > 1 && index(_STAGES, " " substr(key, 1, dash - 1) " ")) { \
2431
+ key = substr(key, dash + 1); \
2432
+ position = index(_TARGETS, " " key "=") \
2433
+ } \
2434
+ if (!position) return ""; \
2435
+ definition = substr(_TARGETS, position + length(key) + 2); \
2436
+ return substr(definition, 1, index(definition, " ") - 1) \
2437
+ } \
2438
+ function render_hyperlink(path, line, absolute_path, definition) { \
2439
+ if (_URL == "") return ""; \
2440
+ definition = target_definition(path); \
2441
+ if (definition != "") path = definition; \
2442
+ else if (path !~ /[\/.]/ || path ~ /[[:space:]]/) return ""; \
2443
+ line = 1; \
2444
+ if (match(path, /:[0-9]+$$/)) { \
2445
+ line = substr(path, RSTART + 1); \
2446
+ path = substr(path, 1, RSTART - 1) \
2447
+ } \
2448
+ if (substr(path, 1, 2) == "./") path = substr(path, 3); \
2449
+ absolute_path = substr(path, 1, 1) == "/" ? path : _CWD "/" path; \
2450
+ if (system("test -f " absolute_path) != 0) return ""; \
2451
+ return "\033]8;;" render_url(_URL, absolute_path, line) "\033\\" \
2452
+ } \
2453
+ { \
2454
+ while (match($$0, /`[^`]*`/)) { \
2455
+ code_start = RSTART; \
2456
+ code_length = RLENGTH; \
2457
+ inline_code = substr($$0, code_start + 1, code_length - 2); \
2458
+ hyperlink = render_hyperlink(inline_code); \
2459
+ hyperlink_end = hyperlink == "" ? "" : "\033]8;;\033\\"; \
2460
+ $$0 = substr($$0, 1, code_start - 1) hyperlink _OPEN inline_code _CLOSE hyperlink_end substr($$0, code_start + code_length) \
2461
+ } \
2462
+ $1 \
2463
+ }'
2464
+ endef
2465
+
2466
+ #**
2467
+ #* Builds an ANSI escape prefix from a combined color and modifier list.
2468
+ #*
2469
+ #* parameters:
2470
+ #* color_and_or_modifiers: AnyOf<normal | white | black | red | yellow | green | cyan | blue | magenta | bold | underline | reverse | bright>
2471
+ #*
2472
+ #* returns: string
2473
+ #*
2474
+ define _ansi
2475
+ $(call _color \
2476
+ ,$(firstword $(filter $(_COLORS),$1)), \
2477
+ $(strip $(filter-out $(firstword $(filter $(_COLORS),$1)),$1)) \
2478
+ )
2479
+ endef
2480
+
2481
+ #**
2482
+ #* The semicolon-separated ANSI codes for a list of modifier tokens.
2483
+ #*
2484
+ #* parameters:
2485
+ #* modifiers: AnyOf<normal | bold | underline | reverse>
2486
+ #*
2487
+ #* returns: string
2488
+ #*
2489
+ define _get_modifiers
2490
+ $(if \
2491
+ $(filter $(MODIFIER_NORMAL),$1),$(_ANSI_MODIFIER_NORMAL),)$(if \
2492
+ $(filter $(MODIFIER_BOLD),$1),$(_ANSI_MODIFIER_BOLD),)$(if \
2493
+ $(filter $(MODIFIER_UNDERLINE),$1),;$(_ANSI_MODIFIER_UNDERLINE),)$(if \
2494
+ $(filter $(MODIFIER_REVERSE),$1),;$(_ANSI_MODIFIER_REVERSE),)
2495
+ endef
2496
+
2497
+ #**
2498
+ #* The ANSI code for a color token, bright when the modifiers ask for it and white when it does not resolve.
2499
+ #*
2500
+ #* parameters:
2501
+ #* color: OneOf<normal | white | black | red | yellow | green | cyan | blue | magenta>
2502
+ #* modifiers?: AnyOf<normal | bold | underline | reverse | bright>
2503
+ #*
2504
+ #* returns: _ANSI_COLOR_*
2505
+ #*
2506
+ define _get_color
2507
+ $(if \
2508
+ $(and $(strip $2),$(or $(if $1,,1),$(filter normal,$1))),$(if \
2509
+ $(filter $(MODIFIER_BRIGHT),$2),$(_ANSI_COLOR_BRIGHT_WHITE),$(_ANSI_COLOR_WHITE)),$(if \
2510
+ $(filter $(MODIFIER_BRIGHT),$2),$(_ANSI_COLOR_BRIGHT_$(_UPPER_$1)),$(_ANSI_COLOR_$(_UPPER_$1))))
2511
+ endef
2512
+
2513
+ #**
2514
+ #* The full ANSI escape sequence for a color and its modifiers.
2515
+ #*
2516
+ #* parameters:
2517
+ #* color: OneOf<normal | white | black | red | yellow | green | cyan | blue | magenta>
2518
+ #* modifiers?: AnyOf<normal | bold | underline | reverse | bright>
2519
+ #*
2520
+ #* returns: string
2521
+ #*
2522
+ define _color
2523
+ $(if $(_IS_NO_ANSI),,\033[$(if $(call _get_modifiers,$2),$(call _get_modifiers,$2);)$(call _get_color,$1,$2)m)
2524
+ endef
2525
+
2526
+ #**
2527
+ #* Converts a string to title case, treating a hyphen as a word break.
2528
+ #*
2529
+ #* parameters:
2530
+ #* text: string
2531
+ #*
2532
+ #* returns: string
2533
+ #*
2534
+ define _to_title_case
2535
+ $(shell $(PRINTF) '%s\n' '$1' | \
2536
+ $(AWK) '{ \
2537
+ gsub(/-/, " "); \
2538
+ for (i = 1; i <= NF; i += 1 ) { \
2539
+ $$i = toupper(substr($$i, 1, 1)) \
2540
+ substr($$i, 2) \
2541
+ } \
2542
+ } \
2543
+ 1' \
2544
+ )
2545
+ endef
2546
+
2547
+ #**
2548
+ #* The argument tokens belonging to a target: those before any target, and those following it.
2549
+ #*
2550
+ #* parameters:
2551
+ #* target: string
2552
+ #* goals: list<string>
2553
+ #*
2554
+ #* returns: string
2555
+ #*
2556
+ define _collect_args
2557
+ $(eval _ca_phase := before)$(eval _ca_shared :=)$(eval _ca_own :=)$(foreach \
2558
+ WORD,$(filter-out --,$2),$(if \
2559
+ $(filter before,$(_ca_phase)),$(if \
2560
+ $(filter $(WORD),$(_ALL_TARGETS)),$(if \
2561
+ $(filter $(WORD),$1),$(eval _ca_phase := own),$(eval _ca_phase := skip)),$(eval \
2562
+ _ca_shared += $(subst $$,$$$$,$(WORD)))),$(if \
2563
+ $(filter own,$(_ca_phase)),$(if \
2564
+ $(filter $(WORD),$(_ALL_TARGETS)),$(eval _ca_phase := done),$(eval \
2565
+ _ca_own += $(subst $$,$$$$,$(WORD)))),$(if \
2566
+ $(filter skip,$(_ca_phase)),$(if \
2567
+ $(filter $(WORD),$(_ALL_TARGETS)),$(if \
2568
+ $(filter $(WORD),$1),$(eval _ca_phase := own),))))))$(_ca_own) $(_ca_shared)
2569
+ endef
2570
+
2571
+ #**
2572
+ #* Every UPPER_SNAKE variable as `NAME="value"` shell assignments, escaped and flattened to one line.
2573
+ #*
2574
+ #* returns: string
2575
+ #*
2576
+ define _make_vars_as_env
2577
+ $(foreach VARIABLE,$(shell $(AWK) -v _VAR_NAMES="$(.VARIABLES)" '\
2578
+ BEGIN { \
2579
+ count = split(_VAR_NAMES, names, " "); \
2580
+ for (idx = 1; idx <= count; idx++) \
2581
+ if (names[idx] ~ /^_?[A-Z][A-Z0-9_]*$$/) \
2582
+ print names[idx] \
2583
+ }' \
2584
+ ),$(if $(filter simple recursive,$(flavor $(VARIABLE))), \
2585
+ $(VARIABLE)="$(subst $$,\$$,$(subst ",\",$(subst `,\`,$(subst \,\\,$(subst $(_NEWLINE), ,$($(VARIABLE)))))))" \
2586
+ ))
2587
+ endef
2588
+
2589
+ #**
2590
+ #* Returns the detected editor name by inspecting well-known terminal environment variables.
2591
+ #*
2592
+ #* returns: OneOf<vscode | phpstorm> | empty-string
2593
+ #*
2594
+ define _editor_from_env
2595
+ $(if $(filter vscode,$(TERM_PROGRAM)),$(EDITOR_VSCODE),$(if \
2596
+ $(VSCODE_PID),$(EDITOR_VSCODE),$(if \
2597
+ $(filter JetBrains-JediTerm,$(TERMINAL_EMULATOR)),$(EDITOR_PHPSTORM),)))
2598
+ endef
2599
+
2600
+ #**
2601
+ #* Returns the detected editor name based on command availability in PATH.
2602
+ #*
2603
+ #* returns: OneOf<vscode | phpstorm> | empty-string
2604
+ #*
2605
+ define _editor_from_command
2606
+ $(if $(shell command -v code >/dev/null 2>&1 && echo 1),$(EDITOR_VSCODE),$(if \
2607
+ $(shell { command -v pstorm || command -v phpstorm; } >/dev/null 2>&1 && echo 1),$(EDITOR_PHPSTORM),))
2608
+ endef
2609
+
2610
+ #---vvv programs
2611
+
2612
+ define _GITATTRIBUTES_PROGRAM_SOURCE
2613
+ $$attributesPath = $$argv[1];
2614
+ $$manifest = json_decode((string) file_get_contents($$argv[2]), true, flags: JSON_THROW_ON_ERROR);
2615
+
2616
+ $$shippedPaths = ['composer.json', 'LICENSE', 'README.md'];
2617
+ $$declaredPaths = array_filter(explode(' ', $$argv[3] ?? ''));
2618
+
2619
+ foreach (['psr-4', 'psr-0', 'classmap', 'files'] as $$section) {
2620
+ foreach ((array) ($$manifest['autoload'][$$section] ?? []) as $$sectionPaths) {
2621
+ foreach ((array) $$sectionPaths as $$sectionPath) {
2622
+ $$shippedPaths[] = $$sectionPath;
2623
+ }
2624
+ }
2625
+ }
2626
+
2627
+ foreach ((array) ($$manifest['bin'] ?? []) as $$binaryPath) {
2628
+ $$shippedPaths[] = $$binaryPath;
2629
+ }
2630
+
2631
+ $$projectDirectory = dirname($$argv[2]);
2632
+ $$expand = static function (array $$paths, bool $$wholeTree) use ($$projectDirectory): array {
2633
+ $$expanded = [];
2634
+
2635
+ foreach ($$paths as $$path) {
2636
+ $$path = (string) preg_replace('#^(?:\./)+#', '', $$path);
2637
+ $$isWholeTree = $$wholeTree
2638
+ ? is_dir($$projectDirectory . '/' . trim($$path, '/'))
2639
+ : str_ends_with($$path, '/');
2640
+ $$path = '/' . trim($$path, '/');
2641
+ $$expanded[] = $$path;
2642
+
2643
+ if ($$isWholeTree) {
2644
+ $$expanded[] = $$path . '/**';
2645
+ }
2646
+ }
2647
+
2648
+ return $$expanded;
2649
+ };
2650
+
2651
+ $$shippedPaths = [...$$expand($$shippedPaths, true), ...$$expand($$declaredPaths, false)];
2652
+ $$shippedLookup = array_flip($$shippedPaths);
2653
+
2654
+ foreach ($$shippedPaths as $$path) {
2655
+ while (($$path = dirname($$path)) !== '/' && $$path !== '.') {
2656
+ $$shippedPaths[] = $$path;
2657
+ }
2658
+ }
2659
+
2660
+ $$shippedPaths = array_values(array_unique($$shippedPaths));
2661
+
2662
+ sort($$shippedPaths);
2663
+
2664
+ $$pathWidth = max(array_map(strlen(...), [...$$shippedPaths, '*']));
2665
+ $$renderedLines = [
2666
+ sprintf('%-' . $$pathWidth . 's export-ignore', '*'),
2667
+ sprintf('%-' . $$pathWidth . 's export-ignore', '.*'),
2668
+ ];
2669
+
2670
+ foreach ($$shippedPaths as $$path) {
2671
+ $$renderedLines[] = sprintf('%-' . $$pathWidth . 's -export-ignore', $$path)
2672
+ . (isset($$shippedLookup[$$path]) ? '' : ' -export-subst');
2673
+ }
2674
+
2675
+ $$currentContents = (string) file_get_contents($$attributesPath);
2676
+ $$generatedRegion = "###> brnshkr/config ###\n" . implode("\n", $$renderedLines) . "\n###< brnshkr/config ###";
2677
+ $$updatedContents = preg_replace('/###> brnshkr\/config ###.*###< brnshkr\/config ###/s', $$generatedRegion, $$currentContents, 1);
2678
+
2679
+ if ($$updatedContents !== null && $$updatedContents !== $$currentContents) {
2680
+ file_put_contents($$attributesPath, $$updatedContents);
2681
+ echo $$attributesPath;
2682
+ }
2683
+ endef
2684
+
2685
+ define _NAME_INTERNAL_TAG_PROGRAM_SOURCE
2686
+ require $$argv[1];
2687
+
2688
+ $$path = $$argv[2];
2689
+ $$namespace = Brnshkr\Config\ComposerJson::forProjectUsingThisLibrary()->getRootNamespace() ?? 'App';
2690
+
2691
+ file_put_contents($$path, preg_replace('/@internal(?: \S+)?$$/m', '@internal ' . $$namespace, file_get_contents($$path), 1));
2692
+ endef
2693
+
2694
+ #**
2695
+ #* A release body: the file's section where there is one, without its heading and with the compare link.
2696
+ #*
2697
+ #* returns: void
2698
+ #*
2699
+ define _changelog_notes
2700
+ section=$$([ ! -f '$(_CHANGELOG_FILE)' ] || _CHANGELOG_VERSION='$(_CHANGELOG_VERSION)' \
2701
+ $(AWK) "$$_CHANGELOG_SECTION_AWK" '$(_CHANGELOG_FILE)'); \
2702
+ if [ -n "$$section" ]; then \
2703
+ $(CAT) >/dev/null; \
2704
+ $(PRINTF) '%s\n' "$$section"; \
2705
+ else \
2706
+ $(AWK) 'NR == 1 && /^## / { next } { body = body $$0 "\n" } \
2707
+ END { sub(/^\n+/, "", body); printf "%s", body }'; \
2708
+ fi; \
2709
+ [ -z '$(_CHANGELOG_COMPARE)' ] || $(PRINTF) '\n**Full Changelog**: %s\n' '$(_CHANGELOG_COMPARE)'
2710
+ endef
2711
+
2712
+ #**
2713
+ #* Prints the generated changelog under the title, as the file carries it.
2714
+ #*
2715
+ #* returns: void
2716
+ #*
2717
+ define _changelog_print
2718
+ { $(PRINTF) '%s\n\n' '$(CHANGELOG_TITLE)'; $(CAT); }
2719
+ endef
2720
+
2721
+ #**
2722
+ #* Merges the generated sections into `CHANGELOG_DIR`, keeping every note already written there.
2723
+ #*
2724
+ #* returns: void
2725
+ #*
2726
+ define _changelog_write
2727
+ if [ '$(_CHANGELOG_MAJOR)' = '0' ] \
2728
+ && ! $(call confirm,`$(_CHANGELOG_VERSION)` is below `1.0.0`$(_COMMA) write it anyway?); then \
2729
+ $(CAT) >/dev/null; \
2730
+ $(call log,Nothing written.,$(COLOR_NOTICE)); \
2731
+ else \
2732
+ $(MKDIR) -p '$(CHANGELOG_DIR)'; \
2733
+ $(CAT) > '$(_CHANGELOG_FILE).added'; \
2734
+ { \
2735
+ $(AWK) 'NR == 1 { has_title = $$0 == "$(CHANGELOG_TITLE)" } END { exit !has_title }' \
2736
+ '$(_CHANGELOG_FILE)' 2>/dev/null \
2737
+ || $(PRINTF) '%s\n\n' '$(CHANGELOG_TITLE)'; \
2738
+ _CHANGELOG_ADDED='$(_CHANGELOG_FILE).added' \
2739
+ _CHANGELOG_FORCE='$(filter force f,$(_ARG_WORDS))' \
2740
+ $(AWK) "$$_CHANGELOG_MERGE_AWK" \
2741
+ '$(_CHANGELOG_FILE).added' \
2742
+ $$([ -f '$(_CHANGELOG_FILE)' ] && $(PRINTF) '%s' '$(_CHANGELOG_FILE)'); \
2743
+ } > '$(_CHANGELOG_FILE).new'; \
2744
+ $(RM) -f '$(_CHANGELOG_FILE).added'; \
2745
+ if [ ! -f '$(_CHANGELOG_FILE)' ]; then \
2746
+ $(MV) '$(_CHANGELOG_FILE).new' '$(_CHANGELOG_FILE)' \
2747
+ && $(call log,Created `%s`.,$(COLOR_SUCCESS),'$(call _named_path,$(_CHANGELOG_FILE))'); \
2748
+ elif $(CMP) -s '$(_CHANGELOG_FILE).new' '$(_CHANGELOG_FILE)'; then \
2749
+ $(RM) -f '$(_CHANGELOG_FILE).new'; \
2750
+ $(call log,Nothing written.,$(COLOR_NOTICE)); \
2751
+ else \
2752
+ $(MV) '$(_CHANGELOG_FILE).new' '$(_CHANGELOG_FILE)' \
2753
+ && $(call log,Updated `%s`.,$(COLOR_SUCCESS),'$(call _named_path,$(_CHANGELOG_FILE))'); \
2754
+ fi; \
2755
+ fi
2756
+ endef
2757
+
2758
+ define _CHANGELOG_SECTION_AWK_SOURCE
2759
+ function version_of(line, value) {
2760
+ value = line
2761
+ sub(/^## \[?/, "", value)
2762
+ sub(/[]( ].*$$/, "", value)
2763
+ return value
2764
+ }
2765
+ BEGIN { wanted = ENVIRON["_CHANGELOG_VERSION"] }
2766
+ /^## / {
2767
+ inside = (version_of($$0) == wanted)
2768
+ next
2769
+ }
2770
+ inside { body = body $$0 "\n" }
2771
+ END {
2772
+ sub(/^\n+/, "", body)
2773
+ sub(/\n+$$/, "", body)
2774
+ if (body != "") print body
2775
+ }
2776
+ endef
2777
+
2778
+ define _CHANGELOG_MERGE_AWK_SOURCE
2779
+ function pad(value, padded) {
2780
+ padded = "0000000000" value
2781
+ return substr(padded, length(padded) - 9)
2782
+ }
2783
+ function version_of(line, value) {
2784
+ value = line
2785
+ sub(/^## \[?/, "", value)
2786
+ sub(/[]( ].*$$/, "", value)
2787
+ return value
2788
+ }
2789
+ function semver_key(version, value, prerelease, parts, count, position, key, tokens, token_count, token) {
2790
+ value = version
2791
+ sub(/^v/, "", value)
2792
+ sub(/\+.*$$/, "", value)
2793
+ prerelease = ""
2794
+ if (match(value, /-/)) {
2795
+ prerelease = substr(value, RSTART + 1)
2796
+ value = substr(value, 1, RSTART - 1)
2797
+ }
2798
+ count = split(value, parts, ".")
2799
+ key = ""
2800
+ for (position = 1; position <= 3; position += 1) key = key pad((position <= count) ? parts[position] + 0 : 0)
2801
+ if (prerelease == "") return key "1"
2802
+ key = key "0"
2803
+ token_count = split(prerelease, tokens, ".")
2804
+ for (position = 1; position <= token_count; position += 1) {
2805
+ token = tokens[position]
2806
+ key = key ((token ~ /^[0-9]+$$/) ? "0" pad(token + 0) : "1" substr(token "..........", 1, 10))
2807
+ }
2808
+ return key
2809
+ }
2810
+ function hash_of(line, value) {
2811
+ if (!match(line, /\(\[?[0-9a-f]{7}\]?[)(]/)) return ""
2812
+ value = substr(line, RSTART, RLENGTH)
2813
+ gsub(/[^0-9a-f]/, "", value)
2814
+ return value
2815
+ }
2816
+ function is_entry(line) {
2817
+ return (line ~ /^- /) && hash_of(line) != ""
2818
+ }
2819
+ function is_heading(line) {
2820
+ return line ~ /^#{3,4} /
2821
+ }
2822
+
2823
+ function section_text(index_, position, out) {
2824
+ out = ""
2825
+ for (position = 1; position <= added_lines[index_]; position += 1) out = out added_line[index_, position] "\n"
2826
+ sub(/\n+$$/, "", out)
2827
+ return out "\n"
2828
+ }
2829
+
2830
+ function merged_text(existing_index, added_index, position, entry, group, pending, line, out, held, tail, seen_entry, regenerated) {
2831
+ pending = 0
2832
+ for (position = 1; position <= added_entries[added_index]; position += 1) {
2833
+ if ((existing_index, added_entry_hash[added_index, position]) in existing_hash) continue
2834
+ pending += 1
2835
+ pending_group[pending] = added_entry_group[added_index, position]
2836
+ pending_entry[pending] = added_entry[added_index, position]
2837
+ }
2838
+ out = ""
2839
+ group = ""
2840
+ held = ""
2841
+ tail = ""
2842
+ seen_entry = 0
2843
+ for (position = 1; position <= existing_lines[existing_index]; position += 1) {
2844
+ line = existing_line[existing_index, position]
2845
+ if (is_heading(line) || line ~ /^## /) {
2846
+ out = out flush_group(group, held, tail, seen_entry, pending)
2847
+ group = is_heading(line) ? line : ""
2848
+ held = ""
2849
+ tail = ""
2850
+ seen_entry = 0
2851
+ out = out line "\n"
2852
+ continue
2853
+ }
2854
+ if (is_entry(line)) {
2855
+ seen_entry = 1
2856
+ regenerated = ENVIRON["_CHANGELOG_FORCE"] == "" ? "" : added_by_hash[added_index, group, hash_of(line)]
2857
+ held = held tail (regenerated == "" ? line : regenerated) "\n"
2858
+ tail = ""
2859
+ } else if (seen_entry) {
2860
+ tail = tail line "\n"
2861
+ } else {
2862
+ out = out line "\n"
2863
+ }
2864
+ }
2865
+ out = out flush_group(group, held, tail, seen_entry, pending)
2866
+ for (position = 1; position <= pending; position += 1) {
2867
+ if (pending_used[position]) continue
2868
+ out = out "\n" (pending_group[position] == "" ? "" : pending_group[position] "\n\n") pending_entry[position] "\n"
2869
+ }
2870
+ split("", pending_used)
2871
+ sub(/\n+$$/, "", out)
2872
+ return out "\n"
2873
+ }
2874
+
2875
+ function flush_group(group, held, tail, seen_entry, pending, position, out) {
2876
+ out = held
2877
+ for (position = 1; position <= pending; position += 1) {
2878
+ if (pending_used[position] || pending_group[position] != group) continue
2879
+ out = out pending_entry[position] "\n"
2880
+ pending_used[position] = 1
2881
+ }
2882
+ return out tail
2883
+ }
2884
+
2885
+ # Every section the generator produced, with its entries and the group each sits under.
2886
+ BEGIN { _added = ENVIRON["_CHANGELOG_ADDED"] }
2887
+ FILENAME == _added {
2888
+ if ($$0 ~ /^## /) {
2889
+ added_count += 1
2890
+ added_version[added_count] = version_of($$0)
2891
+ group = ""
2892
+ } else if (is_heading($$0)) {
2893
+ group = $$0
2894
+ }
2895
+ if (added_count == 0) next
2896
+ added_lines[added_count] += 1
2897
+ added_line[added_count, added_lines[added_count]] = $$0
2898
+ if (is_entry($$0)) {
2899
+ added_entries[added_count] += 1
2900
+ added_entry[added_count, added_entries[added_count]] = $$0
2901
+ added_entry_group[added_count, added_entries[added_count]] = group
2902
+ added_entry_hash[added_count, added_entries[added_count]] = hash_of($$0)
2903
+ added_by_hash[added_count, group, hash_of($$0)] = $$0
2904
+ }
2905
+ next
2906
+ }
2907
+
2908
+ # The file as it stands, kept verbatim.
2909
+ {
2910
+ if ($$0 ~ /^## /) {
2911
+ existing_count += 1
2912
+ existing_version[existing_count] = version_of($$0)
2913
+ }
2914
+ if (existing_count == 0) {
2915
+ preamble = preamble $$0 "\n"
2916
+ next
2917
+ }
2918
+ existing_lines[existing_count] += 1
2919
+ existing_line[existing_count, existing_lines[existing_count]] = $$0
2920
+ if (is_entry($$0)) existing_hash[existing_count, hash_of($$0)] = 1
2921
+ }
2922
+
2923
+ # An existing section, with any entry it is missing placed after the last entry of its own group.
2924
+ END {
2925
+ document = preamble
2926
+
2927
+ for (existing_index = 1; existing_index <= existing_count; existing_index += 1) {
2928
+ matched[existing_version[existing_index]] = existing_index
2929
+ }
2930
+ for (added_index = 1; added_index <= added_count; added_index += 1) {
2931
+ matched_added[added_version[added_index]] = added_index
2932
+ }
2933
+
2934
+ # Sections the file does not carry yet, newest first, placed among the ones it does.
2935
+ for (added_index = 1; added_index <= added_count; added_index += 1) {
2936
+ if (added_version[added_index] in matched) continue
2937
+ insert_count += 1
2938
+ insert_key[insert_count] = semver_key(added_version[added_index])
2939
+ insert_index[insert_count] = added_index
2940
+ }
2941
+ for (position = 2; position <= insert_count; position += 1) {
2942
+ key_held = insert_key[position]
2943
+ value_held = insert_index[position]
2944
+ sorted = position - 1
2945
+ while (sorted >= 1 && insert_key[sorted] < key_held) {
2946
+ insert_key[sorted + 1] = insert_key[sorted]
2947
+ insert_index[sorted + 1] = insert_index[sorted]
2948
+ sorted -= 1
2949
+ }
2950
+ insert_key[sorted + 1] = key_held
2951
+ insert_index[sorted + 1] = value_held
2952
+ }
2953
+
2954
+ pending_insert = 1
2955
+ for (existing_index = 1; existing_index <= existing_count; existing_index += 1) {
2956
+ while (pending_insert <= insert_count \
2957
+ && insert_key[pending_insert] > semver_key(existing_version[existing_index])) {
2958
+ document = document section_text(insert_index[pending_insert]) "\n"
2959
+ pending_insert += 1
2960
+ }
2961
+ replacement = ((existing_version[existing_index] in matched_added) \
2962
+ ? merged_text(existing_index, matched_added[existing_version[existing_index]]) : "")
2963
+ if (replacement == "") {
2964
+ for (position = 1; position <= existing_lines[existing_index]; position += 1) {
2965
+ document = document existing_line[existing_index, position] "\n"
2966
+ }
2967
+ } else {
2968
+ document = document replacement
2969
+ }
2970
+ sub(/\n+$$/, "", document)
2971
+ document = document "\n\n"
2972
+ }
2973
+ while (pending_insert <= insert_count) {
2974
+ document = document section_text(insert_index[pending_insert]) "\n"
2975
+ pending_insert += 1
2976
+ }
2977
+
2978
+ sub(/\n+$$/, "", document)
2979
+ print document
2980
+ }
2981
+ endef
2982
+
2983
+ define _CHANGELOG_AWK_SOURCE
2984
+ function sort_pair(keys, values, count, position, sorted, key_held, value_held) {
2985
+ for (position = 2; position <= count; position += 1) {
2986
+ key_held = keys[position]
2987
+ value_held = values[position]
2988
+ sorted = position - 1
2989
+ while (sorted >= 1 && keys[sorted] > key_held) {
2990
+ keys[sorted + 1] = keys[sorted]
2991
+ values[sorted + 1] = values[sorted]
2992
+ sorted -= 1
2993
+ }
2994
+ keys[sorted + 1] = key_held
2995
+ values[sorted + 1] = value_held
2996
+ }
2997
+ }
2998
+ function readable_scope(scope, parts, count, index_, word, out) {
2999
+ if (scope in names) return names[scope]
3000
+ count = split(scope, parts, /-/)
3001
+ out = ""
3002
+ for (index_ = 1; index_ <= count; index_ += 1) {
3003
+ word = parts[index_]
3004
+ out = out (index_ == 1 ? "" : " ") toupper(substr(word, 1, 1)) substr(word, 2)
3005
+ }
3006
+ return out
3007
+ }
3008
+ function render(section, label, position, inner, seconds, mirrored, keys, entries, second, out) {
3009
+ out = unscoped[section, label]
3010
+ for (position = 1; position <= second_count[section, label]; position += 1) {
3011
+ seconds[position] = second_name[section, label, position]
3012
+ mirrored[position] = second_name[section, label, position]
3013
+ }
3014
+ sort_pair(seconds, mirrored, second_count[section, label])
3015
+ for (position = 1; position <= second_count[section, label]; position += 1) {
3016
+ second = seconds[position]
3017
+ if (out != "") out = out "\n"
3018
+ out = out "#### " readable_scope(second) "\n\n" sub_unscoped[section, label, second]
3019
+ split("", keys)
3020
+ split("", entries)
3021
+ for (inner = 1; inner <= sub_count[section, label, second]; inner += 1) {
3022
+ keys[inner] = sub_key[section, label, second, inner]
3023
+ entries[inner] = sub_entry[section, label, second, inner]
3024
+ }
3025
+ sort_pair(keys, entries, sub_count[section, label, second])
3026
+ for (inner = 1; inner <= sub_count[section, label, second]; inner += 1) out = out entries[inner] "\n"
3027
+ }
3028
+ return out
3029
+ }
3030
+ function major_of(version, value) {
3031
+ value = version
3032
+ sub(/^v/, "", value)
3033
+ sub(/\..*$$/, "", value)
3034
+ return value
3035
+ }
3036
+ function flush(section, version, compare, position, label, printed, out) {
3037
+ if (!(section in has_content)) return ""
3038
+ if (_major != "" && major_of(version) != _major) return ""
3039
+ out = (version == "") ? "" : "## " ((compare != "") ? "[" version "](" compare ")" : version) "\n"
3040
+ if ((section in breaking)) out = out "\n### " breaking_label "\n\n" breaking[section]
3041
+ for (position = 1; position <= rank; position += 1) {
3042
+ label = ordered[position]
3043
+ if (((section, label) in seen) && !(label in printed)) {
3044
+ out = out "\n### " label "\n\n" render(section, label)
3045
+ printed[label] = 1
3046
+ }
3047
+ }
3048
+ if (((section, other) in seen)) out = out "\n### " other "\n\n" render(section, other)
3049
+ return out
3050
+ }
3051
+ BEGIN {
3052
+ RS = "\x1e"
3053
+ FS = "\x1f"
3054
+ other = "📌 Other"
3055
+ breaking_label = "⚠ Breaking changes"
3056
+ _URL = ENVIRON["_CHANGELOG_URL"]
3057
+ _version = ENVIRON["_CHANGELOG_VERSION"]
3058
+ _compare = ENVIRON["_CHANGELOG_COMPARE"]
3059
+ _all = ENVIRON["_CHANGELOG_ALL"]
3060
+ _commits = ENVIRON["_CHANGELOG_COMMITS"]
3061
+ _types = ENVIRON["_CHANGELOG_TYPES"]
3062
+ _major = ENVIRON["_CHANGELOG_MAJOR"]
3063
+ section = 0
3064
+ section_version[0] = _version
3065
+ pair_count = split(ENVIRON["_CHANGELOG_GROUPS"], pairs, " ")
3066
+ for (index_ = 1; index_ <= pair_count; index_ += 1) {
3067
+ if (pairs[index_] !~ /=/) continue
3068
+ root = pairs[index_]
3069
+ sub(/=.*$$/, "", root)
3070
+ label = pairs[index_]
3071
+ sub(/^[^=]*=/, "", label)
3072
+ gsub(/_/, " ", label)
3073
+ groups[root] = label
3074
+ if (!(label in ranked)) { ranked[label] = ++rank; ordered[rank] = label }
3075
+ }
3076
+ name_count = split(ENVIRON["_CHANGELOG_NAMES"], pairs, " ")
3077
+ for (index_ = 1; index_ <= name_count; index_ += 1) {
3078
+ if (pairs[index_] !~ /=/) continue
3079
+ scope_name = pairs[index_]
3080
+ sub(/=.*$$/, "", scope_name)
3081
+ readable = pairs[index_]
3082
+ sub(/^[^=]*=/, "", readable)
3083
+ gsub(/_/, " ", readable)
3084
+ names[scope_name] = readable
3085
+ }
3086
+ }
3087
+ {
3088
+ hash = $$1
3089
+ refs = $$2
3090
+ subject = $$3
3091
+ body = $$4
3092
+ sub(/^\n+/, "", hash)
3093
+
3094
+ if (_all != "" && match(refs, /tag: [^,]+/)) {
3095
+ tag = substr(refs, RSTART + 5, RLENGTH - 5)
3096
+ section_compare[section] = (_URL != "") ? _URL "/compare/" tag "..." section_version[section] : ""
3097
+ section += 1
3098
+ section_version[section] = tag
3099
+ }
3100
+
3101
+ if (subject !~ /^[a-z]+(\([^)]+\))?!?: /) next
3102
+
3103
+ scope = ""
3104
+ if (subject ~ /^[a-z]+\(/) {
3105
+ scope = subject
3106
+ sub(/^[a-z]+\(/, "", scope)
3107
+ sub(/\).*$$/, "", scope)
3108
+ }
3109
+
3110
+ text = subject
3111
+ sub(/^[^:]+: /, "", text)
3112
+ is_breaking = (subject ~ /^[a-z]+(\([^)]+\))?!:/) || (body ~ /BREAKING[ -]CHANGE/)
3113
+ type = subject
3114
+ sub(/[(!:].*$$/, "", type)
3115
+ if (!is_breaking && index(" " _types " ", " " type " ") > 0) next
3116
+
3117
+ if (!is_breaking && index(" " _commits " ", " " type "(" scope ")" " ") > 0) next
3118
+
3119
+ root = scope
3120
+ sub(/\/.*$$/, "", root)
3121
+
3122
+ is_grouped = (root in groups)
3123
+ label = is_grouped ? groups[root] : other
3124
+ nested = scope
3125
+ if (is_grouped) sub(/^[^\/]+\/?/, "", nested)
3126
+ second = nested
3127
+ sub(/\/.*$$/, "", second)
3128
+ rest = nested
3129
+ sub(/^[^\/]+\/?/, "", rest)
3130
+ short = substr(hash, 1, 7)
3131
+ reference = (_URL != "") ? " ([" short "](" _URL "/commit/" hash "))" : " (" short ")"
3132
+
3133
+ entry = "- "
3134
+ if (rest != "") entry = entry "**" rest ":** "
3135
+ entry = entry text
3136
+ if (is_breaking) entry = entry " — **breaking**"
3137
+ entry = entry reference
3138
+
3139
+ if (is_breaking) {
3140
+ breaking[section] = breaking[section] "- " (scope != "" ? "**" scope ":** " : "") text \
3141
+ " — **breaking**" reference "\n"
3142
+ }
3143
+
3144
+ if (nested == "") {
3145
+ unscoped[section, label] = unscoped[section, label] entry "\n"
3146
+ } else {
3147
+ if (!((section, label, second) in second_seen)) {
3148
+ second_seen[section, label, second] = 1
3149
+ second_count[section, label] += 1
3150
+ second_name[section, label, second_count[section, label]] = second
3151
+ }
3152
+ if (rest == "") {
3153
+ sub_unscoped[section, label, second] = sub_unscoped[section, label, second] entry "\n"
3154
+ } else {
3155
+ sub_count[section, label, second] += 1
3156
+ sub_key[section, label, second, sub_count[section, label, second]] = rest
3157
+ sub_entry[section, label, second, sub_count[section, label, second]] = entry
3158
+ }
3159
+ }
3160
+
3161
+ seen[section, label] = 1
3162
+ has_content[section] = 1
3163
+ }
3164
+ END {
3165
+ for (index_ = 0; index_ <= section; index_ += 1) {
3166
+ rendered = flush(index_, section_version[index_], (_all != "") ? section_compare[index_] : _compare)
3167
+ if (rendered == "") continue
3168
+ document = (document == "") ? rendered : document "\n" rendered
3169
+ }
3170
+ if (document == "") exit
3171
+ sub(/\n+$$/, "", document)
3172
+ print document
3173
+ }
3174
+ endef
3175
+
3176
+ define _GROUP_AWK_SOURCE
3177
+ function repeat(text, times, out) { out = ""; while (times-- > 0) out = out text; return out }
3178
+ function value(record, key, pattern) {
3179
+ pattern = "\"" key "\"[ \t]*:[ \t]*\""
3180
+ if (!match(record, pattern)) return ""
3181
+ record = substr(record, RSTART + RLENGTH)
3182
+ if (!match(record, /^([^"\\]|\\.)*/)) return ""
3183
+ return substr(record, RSTART, RLENGTH)
3184
+ }
3185
+ function unescape(text) {
3186
+ gsub(/\\"/, "\"", text)
3187
+ gsub(/\\\//, "/", text)
3188
+ gsub(/\\\\/, "\\", text)
3189
+ return text
3190
+ }
3191
+ function add(identifier, message) {
3192
+ if (identifier == "" || identifier == "null") return
3193
+ identifier = unescape(identifier)
3194
+ message = unescape(message)
3195
+ if (!(identifier in seen)) { seen[identifier] = 1; order[++total] = identifier }
3196
+ count[identifier] += 1
3197
+ if (example[identifier] == "") example[identifier] = message
3198
+ }
3199
+ BEGIN { if (_IDENTIFIER != "" && _MESSAGE != "") RS = "}" }
3200
+ { raw = raw (NR > 1 ? RS : "") $$0; if ($$0 ~ /(^|\n)[ \t]*[[{]/) has_structured_output = 1 }
3201
+ _IDENTIFIER == "" && match($$0, /(\([0-9]+,[0-9]+\):|:[0-9]+(:[0-9]+)?) error [A-Za-z0-9_.\/-]+:? /) { token = substr($$0, RSTART, RLENGTH); sub(/^[^ ]* error /, "", token); sub(/:? $$/, "", token); add(token, substr($$0, RSTART + RLENGTH)) }
3202
+ _IDENTIFIER != "" && _MESSAGE != "" {
3203
+ pending = pending $$0
3204
+ quotes = pending
3205
+ gsub(/\\./, "", quotes)
3206
+ if (gsub(/"/, "", quotes) % 2) { pending = pending "}"; next }
3207
+ if (index(pending, "\"suppressedMessages\"")) is_in_suppressed_messages = 1
3208
+ if (!is_in_suppressed_messages) add(value(pending, _IDENTIFIER), value(pending, _MESSAGE))
3209
+ if (index(pending, "\"errorCount\"")) is_in_suppressed_messages = 0
3210
+ pending = ""
3211
+ }
3212
+ _IDENTIFIER != "" && _MESSAGE == "" { buffer = buffer " " $$0 }
3213
+ END {
3214
+ pattern = "\"" _IDENTIFIER "\":[ \t]*\\["
3215
+ rest = buffer
3216
+ while (_IDENTIFIER != "" && _MESSAGE == "" && match(rest, pattern)) {
3217
+ items = substr(rest, RSTART + RLENGTH)
3218
+ rest = items
3219
+ sub(/\].*/, "", items)
3220
+ size = split(items, parts, ",")
3221
+ for (item = 1; item <= size; item += 1) {
3222
+ gsub(/^[ \t]*"|"[ \t]*$$/, "", parts[item])
3223
+ add(parts[item], "")
3224
+ }
3225
+ }
3226
+ for (outer = 1; outer < total; outer += 1)
3227
+ for (inner = 1; inner <= total - outer; inner += 1)
3228
+ if (count[order[inner]] < count[order[inner + 1]]) {
3229
+ swap = order[inner]; order[inner] = order[inner + 1]; order[inner + 1] = swap
3230
+ }
3231
+ padding = repeat(" ", 2 * _INDENT)
3232
+ if (total == 0 && (_STATUS != 0 || (_IDENTIFIER != "" && !has_structured_output))) {
3233
+ sub(/\n+$$/, "", raw)
3234
+ gsub(/\n/, "\n" padding, raw)
3235
+ if (raw ~ /[^ \t\n]/) printf "%s%s\n", padding, raw
3236
+ exit _STATUS
3237
+ }
3238
+ if (total == 0) { printf "%s$(call text,✔,$(COLOR_SUCCESS)) $(call text,No findings,$(COLOR_DESCRIPTION))\n", padding; exit }
3239
+ for (position = 1; position <= total; position += 1) {
3240
+ if (length(order[position]) > width) width = length(order[position])
3241
+ if (length(count[order[position]]) > number_width) number_width = length(count[order[position]])
3242
+ findings += count[order[position]]
3243
+ }
3244
+ for (position = 1; position <= total; position += 1) {
3245
+ identifier = order[position]
3246
+ message = example[identifier]
3247
+ printf "%s%s$(call text,%s,$(COLOR_ENTRY)) $(call text,%s,$(COLOR_ERROR))%s", \
3248
+ padding, repeat(" ", number_width - length(count[identifier])), count[identifier], identifier, \
3249
+ (message == "" ? "\n" : repeat(" ", width - length(identifier)) " ")
3250
+ if (message != "") printf "$(call text,%s,$(COLOR_DESCRIPTION))\n", message
3251
+ }
3252
+ printf "%s$(call text,✘,$(COLOR_ERROR)) $(call text,%s %s,$(COLOR_DESCRIPTION))\n", padding, findings, (findings == 1 ? "finding" : "findings")
3253
+ }
3254
+ endef
3255
+
3256
+ define _HELP_AWK_SOURCE
3257
+ function trim(string) { gsub(/^[ \t\r\n]+|[ \t\r\n]+$$/, "", string); return string }
3258
+ function leaf(path) { sub(/.*\//, "", path); return path }
3259
+ function repeat(text, count, out) { out = ""; while (count-- > 0) out = out text; return out }
3260
+ function character_bytes(leading_byte) {
3261
+ return (leading_byte < "\200") ? 1 : ((leading_byte < "\340") ? 2 : ((leading_byte < "\360") ? 3 : 4))
3262
+ }
3263
+ function character_width(character) {
3264
+ if (character < "\340") return 1
3265
+ if (character < "\360") return (character >= "\342\230\200" && character <= "\342\237\277") ? 2 : 1
3266
+ return 2
3267
+ }
3268
+ function display_width(text, position, byte_count, total_width) {
3269
+ total_width = 0
3270
+ for (position = 1; position <= length(text); position += byte_count) {
3271
+ byte_count = character_bytes(substr(text, position, 1))
3272
+ total_width += character_width(substr(text, position, byte_count))
3273
+ }
3274
+ return total_width
3275
+ }
3276
+ function truncate(text, width_limit, position, byte_count, character, taken_width, truncated_text) {
3277
+ if (display_width(text) <= width_limit) return text
3278
+ taken_width = 0
3279
+ truncated_text = ""
3280
+ for (position = 1; position <= length(text); position += byte_count) {
3281
+ byte_count = character_bytes(substr(text, position, 1))
3282
+ character = substr(text, position, byte_count)
3283
+ if (taken_width + character_width(character) > width_limit - 1) break
3284
+ truncated_text = truncated_text character
3285
+ taken_width += character_width(character)
3286
+ }
3287
+ return truncated_text "…"
3288
+ }
3289
+ function render_url(template, file, line) {
3290
+ if (template == "") return ""
3291
+ gsub(/\{cwd\}/, ENVIRON["_CURDIR"], template)
3292
+ gsub(/\{wslDistro\}/, ENVIRON["WSL_DISTRO_NAME"], template)
3293
+ gsub(/\{file\}/, file, template)
3294
+ gsub(/\{line\}/, line, template)
3295
+ return template
3296
+ }
3297
+ function strip_verbosity(text) { sub(/[ \t]*#v+([ \t]|$$)/, "", text); return text }
3298
+ function line_verbosity(line, verbosity_match) { if (line == "") line = $$0; if (!match(line, /#v+([[:space:]]|$$)/)) return 0; verbosity_match = substr(line, RSTART + 1, RLENGTH - 1); sub(/[[:space:]]$$/, "", verbosity_match); if (length(verbosity_match) > max_verbosity) max_verbosity = length(verbosity_match); return length(verbosity_match) }
3299
+ function get_description() { if (!match($$0, /#~~[ ]*/)) return ""; return trim(strip_verbosity(substr($$0, RSTART + RLENGTH))) }
3300
+ function render_inline_code(text, out, ansi_open, ansi_close) {
3301
+ ansi_open = ENVIRON["_INLINE_CODE_OPEN"]
3302
+ ansi_close = ENVIRON["_INLINE_CODE_CLOSE"]
3303
+ out = ""
3304
+ while (match(text, /`[^`]+`/)) {
3305
+ out = out substr(text, 1, RSTART - 1) ansi_open substr(text, RSTART + 1, RLENGTH - 2) ansi_close
3306
+ text = substr(text, RSTART + RLENGTH)
3307
+ }
3308
+ return out text
3309
+ }
3310
+ function derive_description(documentation, line) {
3311
+ match(documentation, /^[^\n]+/)
3312
+ line = tolower(substr(documentation, RSTART, 1)) substr(documentation, RSTART + 1, RLENGTH - 1)
3313
+ sub(/\.[[:space:]]*$$/, "", line)
3314
+ return line
3315
+ }
3316
+ function expand_make(expr, result, varname, previous) {
3317
+ result = expr
3318
+ while (match(result, /\$$[({][A-Za-z0-9_]+[)}]/)) {
3319
+ previous = result
3320
+ varname = substr(result, RSTART + 2, RLENGTH - 3)
3321
+ result = substr(result, 1, RSTART - 1) (varname in ENVIRON ? ENVIRON[varname] : "") substr(result, RSTART + RLENGTH)
3322
+ if (result == previous) break
3323
+ }
3324
+ return result
3325
+ }
3326
+ function does_path_exist(path, line, does_exist) { does_exist = (getline line < path) >= 0; close(path); return does_exist }
3327
+ function eval_wildcard(line, count, paths, i, path) {
3328
+ count = split(substr(line, index(line, "wildcard") + 9), paths, /[[:space:]]+/)
3329
+ for (i = 1; i <= count; i += 1) {
3330
+ path = paths[i]
3331
+ if (!index(path, "/")) continue
3332
+ path = substr(path, index(path, "/"))
3333
+ sub(/[^A-Za-z0-9_.\/-]+$$/, "", path)
3334
+ if (does_path_exist(ENVIRON["_CURDIR"] path)) return 1
3335
+ }
3336
+ return 0
3337
+ }
3338
+ function eval_condition(line, varname) {
3339
+ if (line ~ /^ifneq/ && index(line, "$$(wildcard ")) return eval_wildcard(line)
3340
+ if (match(line, /^ifdef[ \t]+/)) {
3341
+ varname = trim(substr(line, RLENGTH + 1))
3342
+ return (varname in ENVIRON && ENVIRON[varname] != "")
3343
+ }
3344
+ if (line ~ /^ifndef[ \t]+/) return 1
3345
+ return eval_ifeq(line)
3346
+ }
3347
+ function eval_ifeq(line, op_eq, rest, depth, comma, i, c, arg1, arg2, quote) {
3348
+ if (!match(line, /^ifn?eq[ \t]*/)) return 1
3349
+ op_eq = (substr(line, 1, 4) == "ifeq")
3350
+ rest = substr(line, RLENGTH + 1)
3351
+ if (substr(rest, 1, 1) == "\"" || substr(rest, 1, 1) == SQ) {
3352
+ quote = substr(rest, 1, 1)
3353
+ if (!match(rest, "^" quote "[^" quote "]*" quote "[ \t]+" quote "[^" quote "]*" quote)) return 1
3354
+ i = index(substr(rest, 2), quote) + 1
3355
+ arg1 = expand_make(substr(rest, 2, i - 2))
3356
+ rest = substr(rest, i + 1)
3357
+ sub(/^[ \t]+/, "", rest)
3358
+ i = index(substr(rest, 2), substr(rest, 1, 1)) + 1
3359
+ arg2 = expand_make(substr(rest, 2, i - 2))
3360
+ return op_eq ? (arg1 == arg2) : (arg1 != arg2)
3361
+ }
3362
+ if (substr(rest, 1, 1) != "(") return 1
3363
+ for (i = 2; i <= length(rest); i++) {
3364
+ c = substr(rest, i, 1)
3365
+ if (c == "(") depth++
3366
+ else if (c == ")" && depth > 0) depth--
3367
+ else if (c == ")") break
3368
+ else if (c == "," && depth == 0 && !comma) comma = i
3369
+ }
3370
+ arg1 = expand_make(trim(substr(rest, 2, comma - 2)))
3371
+ arg2 = expand_make(trim(substr(rest, comma + 1, i - comma - 1)))
3372
+ return op_eq ? (arg1 == arg2) : (arg1 != arg2)
3373
+ }
3374
+ function begin_condition(line) {
3375
+ condition_depth++
3376
+ if (line ~ /^ifndef[ \t]/) is_in_ifndef = 1
3377
+ is_parent_active = (condition_depth == 1 || condition_is_active[condition_depth - 1])
3378
+ condition_is_active[condition_depth] = is_parent_active ? eval_condition(line) : 0
3379
+ condition_branch_matched[condition_depth] = condition_is_active[condition_depth]
3380
+ }
3381
+ function else_condition(line, else_rest) {
3382
+ if (condition_depth == 0) return
3383
+ is_parent_active = (condition_depth == 1 || condition_is_active[condition_depth - 1])
3384
+ else_rest = line; sub(/^else[ \t]*/, "", else_rest)
3385
+ if (is_parent_active && else_rest ~ /^(ifeq|ifneq|ifdef|ifndef)[ \t]/) {
3386
+ if (condition_branch_matched[condition_depth]) condition_is_active[condition_depth] = 0
3387
+ else {
3388
+ condition_is_active[condition_depth] = eval_condition(else_rest)
3389
+ if (condition_is_active[condition_depth]) condition_branch_matched[condition_depth] = 1
3390
+ }
3391
+ } else if (is_parent_active) condition_is_active[condition_depth] = !condition_branch_matched[condition_depth]
3392
+ }
3393
+ function matches_scope(path, i, f) {
3394
+ if (scope_filter_count == 0) return 1
3395
+ if (path == "main") return 1
3396
+ for (i = 1; i <= scope_filter_count; i += 1) {
3397
+ f = scope_filter[i]
3398
+ if (f == "") continue
3399
+ if (path == f) return 1
3400
+ if (index(path, f "/") == 1) return 1
3401
+ if (index(f, path "/") == 1) return 1
3402
+ }
3403
+ return 0
3404
+ }
3405
+ function is_scope_target(path, i, f) {
3406
+ if (scope_filter_count == 0) return 1
3407
+ for (i = 1; i <= scope_filter_count; i += 1) {
3408
+ f = scope_filter[i]
3409
+ if (f == "") continue
3410
+ if (path == f) return 1
3411
+ if (index(path, f "/") == 1) return 1
3412
+ }
3413
+ return 0
3414
+ }
3415
+ function is_scope_visible(path) { return scope_verbosity[path] + 0 <= verbosity }
3416
+ function scope_has_visible_entries(scope_path, section, idx) {
3417
+ if (!matches_scope(scope_path)) return 0
3418
+ if (!is_scope_target(scope_path)) return 0
3419
+ if (!is_scope_visible(scope_path)) return 0
3420
+ for (idx = 1; idx <= section_count[scope_path, section] + 0; idx += 1)
3421
+ if (section_verbosity[scope_path, section, idx] + 0 <= verbosity) return 1
3422
+ return 0
3423
+ }
3424
+ function push_scope(level, name, verbosity, current, parent_verbosity, parent) {
3425
+ scope_at[level] = (level > 0 && scope_at[level - 1] != "" ? scope_at[level - 1] "/" name : name)
3426
+ scope_depth = level
3427
+ current = scope_at[level]
3428
+ parent_verbosity = (level > 0 ? scope_verbosity[scope_at[level - 1]] + 0 : 0)
3429
+ scope_verbosity[current] = (verbosity > parent_verbosity ? verbosity : parent_verbosity)
3430
+ if (current in seen_scope) return
3431
+ seen_scope[current] = 1
3432
+ parent = (level > 0 ? scope_at[level - 1] : "")
3433
+ if (parent == "" || parent == "main") {
3434
+ if (parent == "main" && !("main" in seen_scope)) { seen_scope["main"] = 1; top_count += 1; top_order[top_count] = "main" }
3435
+ top_count += 1
3436
+ top_order[top_count] = current
3437
+ } else {
3438
+ child_count[parent] += 1
3439
+ children[parent, child_count[parent]] = current
3440
+ }
3441
+ }
3442
+ function alias_of(name, map, position, remainder) {
3443
+ map = " " ENVIRON["_TARGET_ALIAS_MAP"] " "
3444
+ position = index(map, " " name "=")
3445
+ if (!position) return ""
3446
+ remainder = substr(map, position + length(name) + 2)
3447
+ return ", " substr(remainder, 1, index(remainder, " ") - 1)
3448
+ }
3449
+ function add_dotenv_variables( keys, key_count, key_idx, name) {
3450
+ key_count = split(ENVIRON["_DOTENV_FILE_KEYS"], keys, /[[:space:]]+/)
3451
+ scope_depth = 0
3452
+ scope_at[0] = "main"
3453
+ for (key_idx = 1; key_idx <= key_count; key_idx += 1) {
3454
+ name = keys[key_idx]
3455
+ if (name == "" || name in scraped_variable || !(name in ENVIRON)) continue
3456
+ add("variables", name, ENVIRON[name], "?=", "", 0, 0)
3457
+ section_file["main", "variables", section_count["main", "variables"]] = ""
3458
+ }
3459
+ }
3460
+ function resolve_scope( current) {
3461
+ current = (scope_depth > 0 ? scope_at[scope_depth] : scope_at[0] != "" ? scope_at[0] : "main")
3462
+ if (current == "main" && !("main" in seen_scope)) { seen_scope["main"] = 1; top_count += 1; top_order[top_count] = "main" }
3463
+ return current
3464
+ }
3465
+ function add(section, name, text, extra, description, line, entry_verbosity, current, idx) {
3466
+ current = resolve_scope()
3467
+ if (!current) return
3468
+ if (name != "" && seen_symbol[current, section, name]++) return
3469
+ idx = ++section_count[current, section]
3470
+ section_name[current, section, idx] = name (section == "commands" ? alias_of(name) : "")
3471
+ section_text[current, section, idx] = text
3472
+ section_extra[current, section, idx] = extra
3473
+ section_description[current, section, idx] = description
3474
+ section_file[current, section, idx] = FILENAME
3475
+ section_line[current, section, idx] = line
3476
+ section_verbosity[current, section, idx] = (entry_verbosity != "" ? entry_verbosity : line_verbosity())
3477
+ }
3478
+ function handle_define(documentation, description) {
3479
+ if ($$2 ~ /^_/) return
3480
+ if ($$2 ~ /^[A-Z][A-Z0-9_]*$$/) {
3481
+ add("variables", $$2, "(macro)", (is_in_ifndef ? "?=" : ":="), get_description(), FNR)
3482
+ scraped_variable[$$2] = 1
3483
+ } else {
3484
+ description = get_description()
3485
+ if (documentation != "") add("functions", $$2, documentation, (description != "" ? description : derive_description(documentation)), "", FNR)
3486
+ else add("functions", $$2, "", description, "", FNR)
3487
+ }
3488
+ }
3489
+ function has_content(scope_path, section, idx) {
3490
+ if (!matches_scope(scope_path)) return 0
3491
+ if (scope_has_visible_entries(scope_path, section)) return 1
3492
+ for (idx = 1; idx <= child_count[scope_path]; idx += 1)
3493
+ if (has_content(children[scope_path, idx], section)) return 1
3494
+ return 0
3495
+ }
3496
+ function has_any_content(scope_path) {
3497
+ return has_content(scope_path, "description") || has_content(scope_path, "variables") \
3498
+ || has_content(scope_path, "functions") || has_content(scope_path, "commands")
3499
+ }
3500
+ function lowest_verbosity(scope_path, shown, level) {
3501
+ shown = verbosity
3502
+ for (level = 0; level <= max_verbosity; level += 1) { verbosity = level; if (has_any_content(scope_path)) break }
3503
+ verbosity = shown
3504
+ return (level <= max_verbosity ? level : -1)
3505
+ }
3506
+ function tree_max(scope_path, depth, section, mode, best, idx, width, child_width, value) {
3507
+ if (!matches_scope(scope_path)) return 0
3508
+ if (is_scope_visible(scope_path) && is_scope_target(scope_path)) {
3509
+ for (idx = 1; idx <= section_count[scope_path, section] + 0; idx += 1) {
3510
+ if (section_verbosity[scope_path, section, idx] + 0 > verbosity) continue
3511
+ if (mode == "value" && section_description[scope_path, section, idx] == "") continue
3512
+ value = section_text[scope_path, section, idx]
3513
+ if (mode == "value" && ENVIRON["_HAS_RESOLVE_ARGUMENT"] != "" && section_name[scope_path, section, idx] in ENVIRON) value = trim(ENVIRON[section_name[scope_path, section, idx]])
3514
+ width = (mode == "value") ? length(value) : (depth + 2) * ENVIRON["INDENT"] + length(section_name[scope_path, section, idx])
3515
+ if (width > best) best = width
3516
+ }
3517
+ }
3518
+ for (idx = 1; idx <= child_count[scope_path]; idx += 1) {
3519
+ child_width = tree_max(children[scope_path, idx], depth + 1, section, mode)
3520
+ if (child_width > best) best = child_width
3521
+ }
3522
+ return best
3523
+ }
3524
+ function walk(scope_path, depth, section, column, render_scope, padding, width, format, idx, entry, text, extra, description, count, lines, line_idx, has_pending_blank, was_function_printed) {
3525
+ if (!matches_scope(scope_path)) return
3526
+ if (!has_content(scope_path, section)) return
3527
+ if (scope_path != "main") {
3528
+ printf "%s$(call text,%s,$(COLOR_SCOPE))\n", repeat(" ", (depth + 1) * ENVIRON["INDENT"]), leaf(scope_path)
3529
+ }
3530
+ render_scope = scope_has_visible_entries(scope_path, section)
3531
+ if (render_scope) {
3532
+ padding = repeat(" ", (depth + 2) * ENVIRON["INDENT"])
3533
+ width = column - (depth + 2) * ENVIRON["INDENT"]
3534
+ for (idx = 1; idx <= section_count[scope_path, section] + 0; idx += 1) {
3535
+ if (section_verbosity[scope_path, section, idx] + 0 > verbosity) continue
3536
+ entry = section_name[scope_path, section, idx]
3537
+ text = section_text[scope_path, section, idx]
3538
+ extra = section_extra[scope_path, section, idx]
3539
+ description = section_description[scope_path, section, idx]
3540
+ if (section == "variables" && ENVIRON["_HAS_RESOLVE_ARGUMENT"] != "" && entry in ENVIRON) text = trim(ENVIRON[entry])
3541
+ if (section == "variables") description = render_inline_code(description)
3542
+ else if (section == "commands") text = render_inline_code(text)
3543
+ else if (section == "functions") { extra = render_inline_code(extra); text = render_inline_code(text) }
3544
+ else if (section == "description") text = render_inline_code(text)
3545
+ file = section_file[scope_path, section, idx]
3546
+ line = section_line[scope_path, section, idx]
3547
+ if (file != "" && file !~ /^\//) file = ENVIRON["_CURDIR"] "/" file
3548
+ if (ENVIRON["_WORKDIR"] != ENVIRON["_CURDIR"] && index(file, ENVIRON["_WORKDIR"] "/") == 1) \
3549
+ file = ENVIRON["_CURDIR"] substr(file, length(ENVIRON["_WORKDIR"]) + 1)
3550
+ url = (file != "" ? render_url(ENVIRON["_EDITOR_URL"], file, line) : "")
3551
+ hyperlink = ""
3552
+ hyperlink_end = ""
3553
+ if (url != "" && !ENVIRON["_IS_NO_ANSI"]) {
3554
+ hyperlink = "\033]8;;" url "\033\\"
3555
+ hyperlink_end = "\033]8;;\033\\"
3556
+ }
3557
+ entry_linked = hyperlink entry hyperlink_end
3558
+ entry_pad = width - length(entry)
3559
+ if (entry_pad < 0) entry_pad = 0
3560
+ entry_padded = entry_linked repeat(" ", entry_pad)
3561
+ if (section == "variables") {
3562
+ if (description != "") {
3563
+ text = truncate(text, description_column)
3564
+ format = sprintf("%2s %s%s", extra, text, repeat(" ", description_column - display_width(text)))
3565
+ printf "%s$(call text,%s,$(COLOR_ENTRY)) $(call text,%s (%s),$(COLOR_DESCRIPTION))\n", padding, entry_padded, format, description
3566
+ } else {
3567
+ printf "%s$(call text,%s,$(COLOR_ENTRY)) $(call text,%2s %s,$(COLOR_DESCRIPTION))\n", padding, entry_padded, extra, text
3568
+ }
3569
+ } else if (section == "commands") {
3570
+ printf "%s$(call text,%s,$(COLOR_ENTRY)) $(call text,%s,$(COLOR_DESCRIPTION))\n", padding, entry_padded, text
3571
+ } else if (section == "functions") {
3572
+ if (was_function_printed && verbosity >= 2 && text != "") printf "\n"
3573
+ was_function_printed = (text != "" && verbosity >= 2)
3574
+ if (extra != "" && (verbosity < 2 || text == "")) {
3575
+ printf "%s$(call text,%s,$(COLOR_ENTRY)) $(call text,%s,$(COLOR_DESCRIPTION))\n", padding, entry_padded, extra
3576
+ } else {
3577
+ printf "%s$(call text,%s,$(COLOR_ENTRY))\n", padding, entry_linked
3578
+ }
3579
+ if (verbosity >= 2 && text != "") {
3580
+ count = split(text, lines, "\n")
3581
+ has_pending_blank = 0
3582
+ for (line_idx = 1; line_idx <= count; line_idx += 1) {
3583
+ if (lines[line_idx] == "") { if (verbosity < 3) break; has_pending_blank = 1; continue }
3584
+ if (has_pending_blank) { printf "\n"; has_pending_blank = 0 }
3585
+ printf "%s$(call text,%s,$(COLOR_DESCRIPTION))\n", padding " ", lines[line_idx]
3586
+ }
3587
+ }
3588
+ } else {
3589
+ printf "%s$(call text,%s,$(COLOR_DESCRIPTION))\n", padding, text
3590
+ }
3591
+ }
3592
+ }
3593
+ for (idx = 1; idx <= child_count[scope_path]; idx += 1) {
3594
+ if (!has_content(children[scope_path, idx], section)) continue
3595
+ walk(children[scope_path, idx], depth + 1, section, column)
3596
+ }
3597
+ }
3598
+ function print_section(has_previous, section, label, column, top_idx, top_path, found, needs_separator) {
3599
+ for (top_idx = 1; top_idx <= top_count; top_idx += 1) {
3600
+ top_path = top_order[top_idx]
3601
+ if (!has_content(top_path, section)) continue
3602
+ if (!found++) { if (has_previous) printf "\n"; printf "$(call text,%s,$(COLOR_SECTION_LABEL))\n", label }
3603
+ else if (needs_separator) printf "\n"
3604
+ walk(top_path, 0, section, column)
3605
+ needs_separator = (section == "functions" && verbosity >= 2)
3606
+ }
3607
+ return found + 0
3608
+ }
3609
+ BEGIN { SQ = sprintf("%c", 39); verbosity = ENVIRON["_VERBOSITY"] + 0; max_verbosity = verbosity; seen_scope["main"] = 1; top_count = 1; top_order[1] = "main"; scope_filter_count = (ENVIRON["_SCOPES"] != "" ? split(ENVIRON["_SCOPES"], scope_filter, /[[:space:]]+/) : 0); name_count = split(ENVIRON["_TARGET_NAMES"], name_pairs, /[[:space:]]+/); for (i = 1; i <= name_count; i += 1) { at = index(name_pairs[i], "="); resolved_name[substr(name_pairs[i], 1, at - 1)] = substr(name_pairs[i], at + 1) } }
3610
+ current_file != FILENAME { current_file = FILENAME; for (i in scope_at) delete scope_at[i]; for (i in condition_is_active) delete condition_is_active[i]; for (i in condition_branch_matched) delete condition_branch_matched[i]; scope_depth = 0; is_in_define = 0; is_in_documentation = 0; is_in_ifndef = 0; condition_depth = 0 }
3611
+ /^endef([[:space:]].*)?$$/ { is_in_define = 0; next }
3612
+ is_in_define { next }
3613
+ /^(ifeq|ifneq|ifdef|ifndef)[[:space:]]/ { begin_condition($$0); next }
3614
+ /^else([[:space:]]|$$)/ { else_condition($$0); next }
3615
+ /^endif([[:space:]]|$$)/ { if (condition_depth > 0) { condition_depth--; is_in_ifndef = 0 } next }
3616
+ condition_depth > 0 && !condition_is_active[condition_depth] { next }
3617
+ /^#-+/ { match($$0, /^#-+/); scope_dash_count = RLENGTH - 1; scope_suffix = substr($$0, RLENGTH + 1); match(scope_suffix, /^v*/); scope_verbosity_count = RLENGTH; push_scope(scope_dash_count - 2, trim(substr(scope_suffix, scope_verbosity_count + 1)), scope_verbosity_count); next }
3618
+ /^#\*\*/ { is_in_documentation = 1; documentation_text = ""; next }
3619
+ is_in_documentation {
3620
+ if (/^#\*/) { sub(/^#\*[ ]?/, ""); documentation_text = documentation_text $$0 "\n"; next }
3621
+ if (/^define[ \t]+/) { sub(/\n+$$/, "", documentation_text); handle_define(documentation_text); is_in_documentation = 0; is_in_define = 1; next }
3622
+ if (!/^#/) { is_in_documentation = 0; next }
3623
+ }
3624
+ /^define[[:space:]]+/ { handle_define(""); is_in_define = 1; next }
3625
+ /^#!!/ { sub(/^#!![ ]*/, ""); add("description", "", strip_verbosity($$0), "", "", FNR); next }
3626
+ /^[A-Za-z0-9_]+[ \t]*[?:+!]*=/ {
3627
+ line_number = FNR
3628
+ var_source = $$0
3629
+ var_description = ""
3630
+ var_verbosity = ""
3631
+ if (match(var_source, /#~~[ ]*/)) {
3632
+ var_description = trim(strip_verbosity(substr(var_source, RSTART + RLENGTH)))
3633
+ var_source = substr(var_source, 1, RSTART - 1)
3634
+ }
3635
+ match(var_source, /^[A-Za-z0-9_]+/)
3636
+ var_name = substr(var_source, RSTART, RLENGTH)
3637
+ var_rest = substr(var_source, RSTART + RLENGTH)
3638
+ match(var_rest, /^[ \t]*[?:+!]*=/)
3639
+ var_operator = substr(var_rest, RSTART, RLENGTH)
3640
+ gsub(/[ \t]/, "", var_operator)
3641
+ if (var_name !~ /^_/) {
3642
+ raw = substr(var_source, index(var_source, "=") + 1)
3643
+ while (raw ~ /\\[ \t]*$$/) {
3644
+ sub(/[ \t]*\\[ \t]*$$/, "", raw)
3645
+ if ((getline continuation) <= 0) break
3646
+ sub(/^[ \t]+/, "", continuation)
3647
+ if (var_verbosity == "") {
3648
+ continuation_verbosity = line_verbosity(continuation)
3649
+ if (continuation_verbosity) var_verbosity = continuation_verbosity
3650
+ }
3651
+ if (match(continuation, /#~~[ ]*/)) {
3652
+ if (var_description == "")
3653
+ var_description = trim(strip_verbosity(substr(continuation, RSTART + RLENGTH)))
3654
+ continuation = substr(continuation, 1, RSTART - 1)
3655
+ sub(/[ \t]*\\[ \t]*$$/, "", continuation)
3656
+ if (continuation != "") raw = raw " " continuation
3657
+ break
3658
+ }
3659
+ raw = raw " " continuation
3660
+ }
3661
+ add("variables", var_name, trim(strip_verbosity(raw)), (is_in_ifndef ? "?=" : var_operator), var_description, line_number, var_verbosity)
3662
+ scraped_variable[var_name] = 1
3663
+ }
3664
+ next
3665
+ }
3666
+ function target_name(name) { return (name in resolved_name) ? resolved_name[name] : ENVIRON["_TARGET_PREFIX"] name }
3667
+ { while (match($$0, /\$$\(call _target,[a-zA-Z0-9_.-]+\)/)) $$0 = substr($$0, 1, RSTART - 1) target_name(substr($$0, RSTART + 15, RLENGTH - 16)) substr($$0, RSTART + RLENGTH) }
3668
+ { gsub(/\$$\(_TARGET_PREFIX\)/, ENVIRON["_TARGET_PREFIX"]) }
3669
+
3670
+ /^[A-Za-z0-9_.-]+([ \t]+[A-Za-z0-9_.-]+)*:([^=]|$$)/ {
3671
+ split($$0, parts, ":")
3672
+ description = get_description()
3673
+ count = split(parts[1], names, /[ \t]+/)
3674
+ joined_commands = ""
3675
+ for (i = 1; i <= count; i += 1) {
3676
+ name = names[i]
3677
+ if (name == "" || name ~ /^[_\.]/) continue
3678
+ if (name in command_seen) continue
3679
+ command_seen[name] = 1
3680
+ joined_commands = (joined_commands == "" ? name : joined_commands "/" name)
3681
+ }
3682
+ if (joined_commands != "") add("commands", joined_commands, description, "", "", FNR)
3683
+ next
3684
+ }
3685
+ END {
3686
+ if (ENVIRON["_EDITOR_UNKNOWN"] != "") {
3687
+ editor_count = split(ENVIRON["_EDITORS"], editor_names, " ")
3688
+ editor_choices = ""
3689
+ for (editor_idx = 1; editor_idx <= editor_count; editor_idx += 1)
3690
+ editor_choices = editor_choices (editor_idx == 1 ? "" : "|") editor_names[editor_idx]
3691
+ printf "$(call text,[$(_LABEL)],$(COLOR_ERROR)) Unknown editor \"$(call text,%s,$(COLOR_WARNING))\".\n", ENVIRON["_EDITOR_UNKNOWN"]
3692
+ printf "$(call text,[$(_LABEL)],$(COLOR_NOTICE)) Run with \"$(call text,EDITOR=<%s>,$(MODIFIER_UNDERLINE))\" or \"$(call text,EDITOR=,$(MODIFIER_UNDERLINE))\" to disable.\n", editor_choices
3693
+ exit $(BRNSHKR_CONFIG_ERROR_CODE)
3694
+ }
3695
+ for (scope_path in seen_scope)
3696
+ if (!index(scope_path, "/") && (scope_path ~ /^v+$$/ || index(" " ENVIRON["_HELP_WORDS"] " ", " " scope_path " "))) {
3697
+ printf "$(call text,[$(_LABEL)],$(COLOR_ERROR)) Scope \"$(call text,%s,$(COLOR_WARNING))\" takes a name that help reserves.\n", scope_path
3698
+ printf "$(call text,[$(_LABEL)],$(COLOR_NOTICE)) Rename it; help reads \"$(call text,%s,$(MODIFIER_UNDERLINE))\" as a word of its own.\n", scope_path
3699
+ exit $(BRNSHKR_CONFIG_ERROR_CODE)
3700
+ }
3701
+ unknown_filter_count = 0
3702
+ for (filter_idx = 1; filter_idx <= scope_filter_count; filter_idx += 1) {
3703
+ filter = scope_filter[filter_idx]
3704
+ if (filter == "") continue
3705
+ filter_verbosity = lowest_verbosity(filter)
3706
+ if (filter_verbosity >= 0 && filter_verbosity <= verbosity) continue
3707
+ filter_display = filter; gsub(/\//, ".", filter_display)
3708
+ if (filter_verbosity < 0) unknown_filters[++unknown_filter_count] = filter_display
3709
+ else { printf "$(call text,[$(_LABEL)],$(COLOR_ERROR)) Scope \"$(call text,%s,$(COLOR_WARNING))\" shows nothing below %s.\n", filter_display, repeat("v", filter_verbosity); is_hidden = 1 }
3710
+ }
3711
+ if (is_hidden && !unknown_filter_count) exit $(BRNSHKR_CONFIG_ERROR_CODE)
3712
+ if (unknown_filter_count > 0) {
3713
+ unknown_message = ""
3714
+ for (filter_idx = 1; filter_idx <= unknown_filter_count; filter_idx += 1) {
3715
+ separator = (filter_idx == 1 ? "" : (filter_idx == unknown_filter_count ? " and " : ", "))
3716
+ unknown_message = unknown_message separator sprintf("\"$(call text,%s,$(COLOR_WARNING))\"", unknown_filters[filter_idx])
3717
+ }
3718
+ unknown_label = (unknown_filter_count == 1 ? "scope" : "scopes")
3719
+ printf "$(call text,[$(_LABEL)],$(COLOR_ERROR)) Unknown %s %s.\n", unknown_label, unknown_message
3720
+ printf "$(call text,[$(_LABEL)],$(COLOR_NOTICE)) Run \"$(call text,make help list-scopes,$(MODIFIER_UNDERLINE))\" or \"$(call text,make help ls,$(MODIFIER_UNDERLINE))\" to see available scopes.\n"
3721
+ exit $(BRNSHKR_CONFIG_ERROR_CODE)
3722
+ }
3723
+ if (ENVIRON["_HAS_LIST_SCOPES_ARGUMENT"]) {
3724
+ listed_count = 0
3725
+ for (scope_path in seen_scope) {
3726
+ if (scope_path == "main") continue
3727
+ if (!matches_scope(scope_path)) continue
3728
+ if (!is_scope_target(scope_path) && scope_filter_count > 0) continue
3729
+ if (!has_any_content(scope_path)) continue
3730
+ listed_paths[++listed_count] = scope_path
3731
+ }
3732
+ for (sort_idx = 2; sort_idx <= listed_count; sort_idx += 1) {
3733
+ current_path = listed_paths[sort_idx]; compare_idx = sort_idx - 1
3734
+ while (compare_idx >= 1 && listed_paths[compare_idx] > current_path) {
3735
+ listed_paths[compare_idx + 1] = listed_paths[compare_idx]
3736
+ compare_idx -= 1
3737
+ }
3738
+ listed_paths[compare_idx + 1] = current_path
3739
+ }
3740
+ for (listed_idx = 1; listed_idx <= listed_count; listed_idx += 1) {
3741
+ gsub(/\//, ".", listed_paths[listed_idx])
3742
+ print listed_paths[listed_idx]
3743
+ }
3744
+ exit 0
3745
+ }
3746
+ if (ENVIRON["_HAS_RESOLVE_ARGUMENT"] != "") add_dotenv_variables()
3747
+ logo = ENVIRON["_HEADER_LOGO"]; sub(/__BRNSHKR_LOGO_END__$$/, "", logo); logo_reset = ""
3748
+ if (match(logo, /\033\[[0-9;]*m$$/)) { logo_reset = substr(logo, RSTART); logo = substr(logo, 1, RSTART - 1) }
3749
+ if (length(trim(logo)) > 0) printf "%s%s\n", logo, logo_reset
3750
+ if (length(trim(ENVIRON["_HEADER_TITLE"])) > 0) printf "%s\n\n", ENVIRON["_HEADER_TITLE"]
3751
+ printf "%s\n", ENVIRON["_HEADER_USAGE"]
3752
+ for (top_idx = 1; top_idx <= top_count; top_idx += 1) {
3753
+ top_path = top_order[top_idx]
3754
+ column = tree_max(top_path, 0, "variables", "name"); if (column > variable_column) variable_column = column
3755
+ column = tree_max(top_path, 0, "variables", "value"); if (column > description_column) description_column = column
3756
+ if (description_column > ENVIRON["VALUE_WIDTH"] + 0) description_column = ENVIRON["VALUE_WIDTH"] + 0
3757
+ column = tree_max(top_path, 0, "functions", "name"); if (column > function_column) function_column = column
3758
+ column = tree_max(top_path, 0, "commands", "name"); if (column > command_column) command_column = column
3759
+ }
3760
+ has_printed = 1
3761
+ has_printed += print_section(has_printed, "description", "Description:", 0)
3762
+ has_printed += print_section(has_printed, "variables", "Variables:", variable_column)
3763
+ has_printed += print_section(has_printed, "functions", "Functions:", function_column)
3764
+ has_printed += print_section(has_printed, "commands", "Available commands:", command_column)
3765
+ }
3766
+ endef
3767
+
3768
+ export EDITOR_URL
3769
+
3770
+ $(call _target,cc): _ARG_VALUES = $(_CACHES)
3771
+ $(call _target,changelog): export _CHANGELOG_AWK = $(_CHANGELOG_AWK_SOURCE)
3772
+ $(call _target,changelog): export _CHANGELOG_MERGE_AWK = $(_CHANGELOG_MERGE_AWK_SOURCE)
3773
+ $(call _target,changelog): export _CHANGELOG_SECTION_AWK = $(_CHANGELOG_SECTION_AWK_SOURCE)
3774
+ $(call _target,configs): export _GITATTRIBUTES_PROGRAM = $(_GITATTRIBUTES_PROGRAM_SOURCE)
3775
+ $(call _target,configs): export _NAME_INTERNAL_TAG_PROGRAM = $(_NAME_INTERNAL_TAG_PROGRAM_SOURCE)
3776
+ $(call _target,configs): _ARG_VALUES = $(_CONFIG_NAMES) local l force f
3777
+ $(call _target,help): export _HELP_AWK = $(_HELP_AWK_SOURCE)
3778
+ $(_GROUPS): export _GROUP_AWK = $(_GROUP_AWK_SOURCE)