Injection of version of the computed libraries

Hi,
For debug reasons, I would inject into the source code the version of some dependencies whose are compiled.

I specify lib_deps like

lib_deps = myLibrary@^1.0.0

In source code I would like

printf("%s", MY_LIBRARY_VERSION);

I would get the actual compiled library, not “^1.0.0”.
Is it possible?
The library doesn’t have an embedded constant telling the version and I cannot modify the library.

1 Like

Yes that’s possible using Advanced Scripting, we can scan the project’s library dependencies, and then make them available to the code as macros, which evaluate to strings.

E.g., the platformio.ini

[env:uno]
platform = atmelavr
board = uno
debug_tool = simavr
framework = arduino
lib_deps = 
    adafruit/Adafruit GFX Library @ ^1.10.6
    adafruit/Adafruit BusIO @ ^1.7.2
    Wire
    SPI
extra_scripts = add_lib_info.py

with the file add_lib_info.py in the root of the project

from platformio.builder.tools.piolib import ProjectAsLibBuilder, PackageItem, LibBuilderBase
from SCons.Script import ARGUMENTS  
from shlex import quote
Import("env", "projenv")


# from https://github.com/platformio/platformio-core/blob/develop/platformio/builder/tools/piolib.py
def _correct_found_libs(lib_builders):
    # build full dependency graph
    found_lbs = [lb for lb in lib_builders if lb.dependent]
    for lb in lib_builders:
        if lb in found_lbs:
            lb.search_deps_recursive(lb.get_search_files())
    for lb in lib_builders:
        for deplb in lb.depbuilders[:]:
            if deplb not in found_lbs:
                lb.depbuilders.remove(deplb)

print("Script entry point")

project = ProjectAsLibBuilder(env, "$PROJECT_DIR")

# rescan dependencies just like in py file above. otherwise dependenceis are empty
ldf_mode = LibBuilderBase.lib_ldf_mode.fget(project)
lib_builders = env.GetLibBuilders()
project.search_deps_recursive()
if ldf_mode.startswith("chain") and project.depbuilders:
    _correct_found_libs(lib_builders)

# for debugging
def _print_deps_tree(root, level=0):
    margin = "|   " * (level)
    for lb in root.depbuilders:
        title = "<%s>" % lb.name
        pkg = PackageItem(lb.path)
        if pkg.metadata:
            title += " %s" % pkg.metadata.version
        elif lb.version:
            title += " %s" % lb.version
        print("%s|-- %s" % (margin, title), end="")
        if int(ARGUMENTS.get("PIOVERBOSE", 0)):
            if pkg.metadata and pkg.metadata.spec.external:
                print(" [%s]" % pkg.metadata.spec.url, end="")
            print(" (", end="")
            print(lb.path, end="")
            print(")", end="")
        print("")
        if lb.depbuilders:
            _print_deps_tree(lb, level + 1)

# create a map of all used libraries and their version.
# the structure of the tree is not captured, just library names and versions. 
library_versions = dict()
def get_all_library_dependencies(root, level=0):
    global library_versions
    for lb in root.depbuilders:
        pkg = PackageItem(lb.path)
        lib_name = lb.name
        lib_version = pkg.metadata.version if pkg.metadata else lb.version
        library_versions[str(lib_name)] = str(lib_version)
        if lb.depbuilders:
            get_all_library_dependencies(lb, level + 1)

#print("PRINTING DEP TREE")
#_print_deps_tree(project)

get_all_library_dependencies(project)
print(library_versions)

# convert found library names and versions into macros for code.
# style and formating can be arbitrary: here I chose to hold everything in one big string.
macro_value = ""
for lib, version in library_versions.items(): 
    #print(lib + ": " + version)
    # primitive: simply 'library':'version' format. does not escape anything specifically.
    # this is chosen to work around passing the string as -D argument in the shell.
    # we have to additionally add a backslash before a quote to shell-escape it.
    #lib = lib.replace(" ", "\\ ")
    #lib = lib.replace(" ", "\\ ")
    #macro_value += "\"" + lib + "\":\""+  version +"\","
    #macro_value += "*" + lib + "*:*"+  version +"*,"
    macro_value += "'" + lib + "':'"+  version +"',"
# chop off last comma
macro_value = macro_value[:-1]
# escape it all in quotes
macro_value = "\\\"" + macro_value + "\\\""

# add to build system as macro
print("PLATFORMIO_USED_LIBRARIES = " + str(macro_value))
projenv.Append(CPPDEFINES=[
  ("PLATFORMIO_USED_LIBRARIES", macro_value)
])

def make_macro_name(lib_name):
    lib_name = lib_name.upper()
    lib_name = lib_name.replace(" ", "_")
    return lib_name

# also add all individual library versions
for lib, version in library_versions.items():
    projenv.Append(CPPDEFINES=[
     ("LIB_VERSION_%s" % make_macro_name(lib) , "\\\"" + version + "\\\"")
    ])
    print("LIB_VERSION_%s = %s" % (make_macro_name(lib), version))

With src\main.cpp

#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <SPI.h>
#include <string.h>

/* use provided PLATFORMIO_USED_LIBRARIES macro */ 
/* place string in flash so that it doesn't use up RAM. 
*  but then also needs special handling of PROGMEM strings. */
const char used_libs[] PROGMEM = { PLATFORMIO_USED_LIBRARIES };
__FlashStringHelper* used_libs_str = (__FlashStringHelper *)used_libs; 

/* prints the string that contains all libraries. doesn't require you to know the specific macros. */
void print_used_libraries() {
    Serial.println(F("Used libraries (PLATFORMIO_USED_LIBRARIES)"));
    Serial.println(used_libs_str);
    /* you can search through the string per strchr_P() functions etc */
}

/* prints version of a library that we know is included. */
void print_known_libray() {
    //again uses F() to create a flash-string to save space on the poor Uno..
    Serial.print(F("Adafruit GFX library version: "));
    Serial.println(F(LIB_VERSION_ADAFRUIT_GFX_LIBRARY));
}

void setup(){
    Serial.begin(9600);
}

void loop(){
    print_used_libraries();
    print_known_libray();
    delay(1000);
}

Prints

Used libraries (PLATFORMIO_USED_LIBRARIES)
'Adafruit GFX Library':'1.10.6','Adafruit BusIO':'1.7.2','Wire':'1.0','SPI':'1.0'
Adafruit GFX library version: 1.10.6

The python script encodes all used library names and their exact used versions in the macro PLATFORMIO_USED_LIBRARIES and saves them in a string (that will be saved in PROGMEM / Flash).

It also encodes each library and their version in its own macro, so e.g. “Adafruit GFX Library” of version “1.10.6” is turned into the macro LIB_VERSION_ADAFRUIT_GFX_LIBRARY with value "1.10.6"

Does that solve your problem?

2 Likes

Yes, that’s great!
I can handle the minor customization I need :wink:

Finally I would ask if it possible to do the same thing with Arduino Framework version. In my project I’m using esp32, and I would get the actual version of arduino core (that is 1.0.5).

You better understand your script, where can I found documentation about the first python line?

from platformio.builder.tools.piolib import ProjectAsLibBuilder, PackageItem, LibBuilderBase

I type PackageItem in https://docs.platformio.org/ but I didn’t found anything.

Again through Advanced scripting. Here, the object returned by env.PioPlatform() returns the needed info.

You can add this to the bottom of the script:

from platformio.package.version import get_original_version

platform = env.PioPlatform()
used_packages = platform.dump_used_packages()
for package in used_packages:
    pio_package_version = package["version"] # e.g. "1.70300.191015"
    pio_package_name = package["name"] # e.g. "toolchain-atmelavr"
    # can fail at decoding and return None if package version is not in semver format
    # in these cases the pio package version is already the decoded version
    # e.g. 1.70300.191015 => 7.3.0
    # e.g. 5.1.0 => None
    pio_decoded_version = get_original_version(pio_package_version)
    print("Name '%s' Version: %s" % (package["name"], str(get_original_version(package["version"]))))
    name_converter = lambda name: name.upper().replace(" ", "_").replace("-", "_")
    projenv.Append(CPPDEFINES=[
     ("PIO_PACKAGE_%s_PKG_VERSION" % name_converter(pio_package_name) , "\\\"" + pio_package_version + "\\\"")
    ])
    if pio_decoded_version is not None: 
        projenv.Append(CPPDEFINES=[
        ("PIO_PACKAGE_%s_DECODED_VERSION" % name_converter(pio_package_name) , "\\\"" + pio_decoded_version + "\\\"")
        ])

The code was derived by looking how PlatformIO generates the output for “PACKAGES: …” at the start of the compilation output which lsits all used packages.

PACKAGES:
 - framework-arduinoespressif32 3.10005.210223 (1.0.5)
 - tool-esptoolpy 1.30000.201119 (3.0.0) 
 - toolchain-xtensa32 2.50200.97 (5.2.0)

That’s found here.

And that will generate

-DPIO_PACKAGE_TOOLCHAIN_ATMELAVR_PKG_VERSION="1.70300.191015" -DPIO_PACKAGE_TOOLCHAIN_ATMELAVR_DECODED_VERSION="7.3.0" -DPIO_PACKAGE_FRAMEWORK_ARDUINO_AVR_PKG_VERSION="5.1.0"

As compiler flags / macros for an Arduino Uno project.

For platform = espressfi32 and board = esp32dev it would generate

-DPIO_PACKAGE_TOOLCHAIN_XTENSA32_PKG_VERSION="2.50200.97" -DPIO_PACKAGE_TOOLCHAIN_XTENSA32_DECODED_VERSION="5.2.0" -DPIO_PACKAGE_FRAMEWORK_ARDUINOESPRESSIF32_PKG_VERSION="3.10005.210223" -DPIO_PACKAGE_FRAMEWORK_ARDUINOESPRESSIF32_DECODED_VERSION="1.0.5" -DPIO_PACKAGE_TOOL_ESPTOOLPY_PKG_VERSION="1.30000.201119" -DPIO_PACKAGE_TOOL_ESPTOOLPY_DECODED_VERSION="3.0.0"

I’m accessing internal APIs here from the PlatformIO core. The platformio.builder.tools.piolib package path maps to platformio-core/platformio/builder/tools/piolib.py at develop · platformio/platformio-core · GitHub.

I used this file because it outputs the dependency graph

So I just had to look how it’s stored in those internal objects and did the same logic as in the linked file. Thus it’s only to be found in the open-source implementation, but not regular docs. This was more a searching, experimenting and debugging cycle.

Thanks, both work like a charm