Search code examples
powershellmakefile

How do I rewrite this shell command to call PowerShell only once, and still read my Yaml file in my Makefile?


Introduction

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

Problem

The problem I had when I did that is that it didn't acquire the value and only the text entered.

Objetive

I tried many variants of the above code, but everything remained the same.

Extra dates

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

Summary of the problem

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.

Considerations

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.

Results obtained so far, using the solution from this answer:

enter image description here


Solution

    • 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 \".

      • Set the 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.