I am trying to make Makefile read with the shell command a YAML file which is only 20 lines of code at most.
The problem I am having is that it takes a long time to read the data from the YAML file.
I asked several AI's, as at first I thought the powershell code was a bit slow, but the AI's told me it was because I was using the shell command too much and that it was adding a time overhead.
They told me that the best way to solve it was to make a single shell call and the returned value will be used to set the rest of the values.
I did this
CONFIG := $(shell powershell ‘(Get-Content -Path “config.yaml” -Raw | ConvertFrom-Yaml)’)
AS := $(CONFIG).assembly.assembler
The problem I had when I did that is that it didn't acquire the value and only the text entered.
I tried many variants of the above code, but everything remained the same.
In the end I went back to the beginning and put in the following code
AS := $(shell powershell ‘(Get-Content -Path “config.yaml” -Raw | ConvertFrom-Yaml).assembly.assembler’)
LD := $(shell powershell ‘(Get-Content -Path “config.yaml” -Raw | ConvertFrom-Yaml).assembly.linker’)
ASFLAGS := $(shell powershell ‘(Get-Content -Path “config.yaml” -Raw | ConvertFrom-Yaml).assembly.asflags’)
LDFLAGS := $(shell powershell ‘(Get-Content -Path “config.yaml” -Raw | ConvertFrom-Yaml).assembly.ldflags’)
The problem with this code is that it takes a long time to execute when you put make.
I would like to make it run faster because when I did some tests, I noticed that it took about 2 - 3 seconds.
Here's my config.yaml file
System: Windows
assembly:
assembler: as
linker: gcc
ldflags:
asflags: -Iinclude --64
cflags: -Wall -m64
directories:
- src
- lib
- bin
- scripts
- include
To sum up, I want to get the values in the ‘Assembly’ part of my yaml file, but in a quick way as it takes too long to run my Makefile leaving the code as it is.
I use the powershell-yaml
module to read the yaml file.
I don't have any other modules installed and I don't plan to install any more.
The default shell that make
uses on Windows is cmd.exe
; thus, a call to
powershell.exe
, the Windows PowerShell CLI, such as powershell '(Get-Content -Path “config.yaml” -Raw | ConvertFrom-Yaml)'
does not work, because the '...'
(single-quoted) argument is taken by PowerShell to be a verbatim string literal, and is content is therefore echoed as is; from outside PowerShell (including from cmd.exe
), you need "..."
(double-quoted) enclosure to pass commands.
That said, it is better to set PowerShell as make
's default shell, which not only obviates the need for an unnecessary cmd.exe
process, but allows you to pass just the PowerShell code to the $(shell ...)
function; it is then make
that takes care of enclosing the code in "..."
, if necessary, and escaping any embedded "
as \"
.
SHELL
and .SHELLFLAGS
variables, as shown below.The startup cost of the PowerShell CLI is nontrivial, so it's best to use a single call that extracts all information of interest, and parse it into individual values afterwards.
The following sample Makefile
puts it all together:
Important: After copying & pasting, make sure that the line after foo:
starts with an actual tab character rather than spaces (which is a general requirement for recipe lines); if you end up with spaces instead, running make
won't report any results and (confusingly) state make: Nothing to be done for 'FOO'.
Works verifiably with GNU Make 4.4.1, but presumably also earlier versions, assuming that the following prerequisites are met:
A config.yaml
file with the content shown in your question is present in the current directory.
The powershell-yaml
module that ConvertFrom-Yaml
is a part of must be discoverable by module auto-loading, i.e. it must be located in one of the directories listed in $env:PSModulePath
, so that the cmdlet can be invoked without requiring an explicit Import-Module
call first. This is ensured by default if you installed the module with Install-Module
.
# Define what shell to use and what options to pass to it.
SHELL := powershell.exe
.SHELLFLAGS := -NoProfile -Command
# Run a single PowerShell command that extracts all data of interest.
# Pass-through $ must be escaped as $$
RESULT := $(shell \
$$obj = (Get-Content -Raw config.yaml | ConvertFrom-Yaml).assembly;\
$$obj.assembler; $$obj.linker; $$obj.asflags -replace ' +', '&'; $$obj.ldflags -replace ' +', '&'\
)
# Split the result into the individual values of interest.
AS := $(word 1,$(RESULT))
LD := $(word 2,$(RESULT))
ASFLAGS := $(subst &, ,$(word 3,$(RESULT)))
LDFLAGS := $(subst &, ,$(word 4,$(RESULT)))
# Phony default goal that echoes the resulting variable values,
# using a PowerShell command.
.PHONY: foo
foo:
@'AS=[$(AS)]', 'LD=[$(LD)]', 'ASFLAGS=[$(ASFLAGS)]', 'LDFLAGS=[$(LDFLAGS)]'
# IMPORTANT: Be sure that the line above as well as recipe lines in general
# start with an actual TAB char.
Running make
then yields the following, based on your sample config.yaml
, showing that the values were assigned to variables as intended.:
AS=[as]
LD=[gcc]
ASFLAGS=[-Iinclude --64]
LDFLAGS=[]
Note:
make
replaces newlines in the output from a shell command with spaces, and trims a trailing newline.
As such, given that the .asflags
and .ldflag
property values are likely to contain spaces, their embedded spaces are temporarily replaced with instances of &
(an arbitrarily chosen character), so that the boundaries between the emitted values aren't lost.
This is achieved with -replace
, PowerShell's regular-expression-based string replacement operator.
Also note that use of PowerShell's implicit output behavior, where the results of expressions (or commands) that aren't explicitly captured or redirected are implicitly sent to the success output stream (which the outside world sees as stdout); e.g., $obj.assembler
(escaped as $$obj.assembler
in the Makefile
), automatically outputs the value of said property (no need for an echo
(Write-Output
) call).
On the Makefile
side, the captured output is then split into words by whitespace with the $(word <n>,<value>)
function, with the ASDFLAGS
and LDFLAGS
additionally getting retransformed to their original form by replacing &
chars. with spaces, using the $(subst <find-str>,<replace-str>,<input-string>)
function.
As for what you tried:
As explained at the top, using single-quoting around the command in a PowerShell CLI call from outside PowerShell causes the command to be considered a verbatim string that is simply echoed rather than executed.
Even with that problem out of the way,
AS := $(CONFIG).assembly.assembler
fundamentally cannot work:
make
receives text output from the PowerShell CLI call, so you cannot use object-oriented techniques on it; that is, you cannot access properties on $(CONFIG)
, because it is a nothing but a string, and the .assembly.assembler
part simply becomes a verbatim part of the value assigned to variable AS
, given that make
has no notion of objects and properties.As for the output you received when you tried the solution above:
The implication is that the ConvertFrom-Yaml
wasn't actually available by default in your PowerShell CLI session (see comments re auto-discovery above).
Because the PowerShell CLI - unfortunately - also reports errors via stdout (see GitHub issue #7989), what was (unhelpfully) parsed on the Makefile
side was the error message (in your case, the Spanish equivalent of: "ConvertFrom-Yaml: The term 'ConvertFrom-Yaml' is not recognized as a name of a cmdlet, function, script file, or executable program.")
To avoid or diagnose such problems, do a dry run of your commands directly in PowerShell (first). Given that it is possible to install PowerShell modules for the current user only, be sure to use the same user account for both the dry run and the make
calls.