Aurelien Larcher
2021-01-20 27cdcec330b0fca464c4af841974b4a518f3142c
userland-component: add refactoring and update rules
3 files added
1 files modified
647 ■■■■■ changed files
make-rules/component.mk 30 ●●●●● patch | view | raw | blame | history
make-rules/shared-macros.mk 22 ●●●●● patch | view | raw | blame | history
tools/bass/makefiles.py 361 ●●●●● patch | view | raw | blame | history
tools/userland-component 234 ●●●●● patch | view | raw | blame | history
make-rules/component.mk
New file
@@ -0,0 +1,30 @@
# A simple rule to print the value of any macro.  Ex:
#    $ gmake print-REQUIRED_PACKAGES
# Note that some macros are set on a per target basis, so what you see
# is not always what you get.
print-%:
    @echo '$(subst ','\'',$*=$($*)) (origin: $(origin $*), flavor: $(flavor $*))'
# A simple rule to print only the value of any macro.
print-value-%:
    @echo '$(subst ','\'',$($*))'
# Provide default print package targets for components that do not rely on IPS.
# Define them implicitly so that the definitions do not collide with ips.mk
define print-package-rule
echo $(strip $(PACKAGE_$(1))) | tr ' ' '\n'
endef
COMPONENT_TOOL = $(WS_TOOLS)/userland-component
format:
    @$(COMPONENT_TOOL) --path $(COMPONENT_DIR);
update:
    @if [ "$(VERSION)X" = "X" ]; \
    then $(COMPONENT_TOOL) --path $(COMPONENT_DIR) --bump; \
    else $(COMPONENT_TOOL) --path $(COMPONENT_DIR) --bump $(VERSION); \
    fi;
make-rules/shared-macros.mk
@@ -1376,24 +1376,4 @@
include $(WS_MAKE_RULES)/environment.mk
include $(WS_MAKE_RULES)/depend.mk
# A simple rule to print the value of any macro.  Ex:
#    $ gmake print-REQUIRED_PACKAGES
# Note that some macros are set on a per target basis, so what you see
# is not always what you get.
print-%:
    @echo '$(subst ','\'',$*=$($*)) (origin: $(origin $*), flavor: $(flavor $*))'
# A simple rule to print only the value of any macro.
print-value-%:
    @echo '$(subst ','\'',$($*))'
# Provide default print package targets for components that do not rely on IPS.
# Define them implicitly so that the definitions do not collide with ips.mk
define print-package-rule
echo $(strip $(PACKAGE_$(1))) | tr ' ' '\n'
endef
print-package-%:
    @$(call print-package-rule,$(shell tr '[a-z]' '[A-Z]' <<< $*))
include $(WS_MAKE_RULES)/component.mk
tools/bass/makefiles.py
New file
@@ -0,0 +1,361 @@
#!/usr/bin/python3.5
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
# This is all very naive and will hurt pythonists' eyes.
#
import os
import re
import subprocess
import warnings
from .component import Component
class Keywords(object):
    def __init__(self):
        self.variables = {
            "BUILD_BITS":
            ["NO_ARCH",
             "32",
             "64",
             "32_and_64",
             "64_and_32"],
            "BUILD_STYLE":
                ["ant",
                 "attpackagemake",
                 "cmake",
                 "configure",
                 "gem",
                 "justmake",
                 "makemaker",
                 "meson",
                 "ocaml",
                 "setup.py",
                 "waf"],
            "MK_BITS":
            ["NO_ARCH",
             "32",
             "64",
             "32_and_64"],
            "COMPONENT_NAME": [],
            "COMPONENT_VERSION": [],
            "COMPONENT_REVISION": [],
            "COMPONENT_FMRI": [],
            "COMPONENT_CLASSIFICATION": [],
            "COMPONENT_SUMMARY": [],
            "COMPONENT_PROJECT_URL": [],
            "COMPONENT_SRC": ["$(COMPONENT_NAME)-$(COMPONENT_VERSION)"],
            "COMPONENT_ARCHIVE": [],
            "COMPONENT_ARCHIVE_URL": [],
            "COMPONENT_ARCHIVE_HASH": [],
            "COMPONENT_LICENSE": [],
            "COMPONENT_LICENSE_FILE": []
        }
        self.targets = {
            "build": [ "BUILD_$(MK_BITS)"],
            "install": ["INSTALL_$(MK_BITS)"],
            "test": ["TEST_$(MK_BITS)", "NO_TESTS"],
            "system-test": ["SYSTEM_TEST_$(MK_BITS)", "SYSTEM_TESTS_NOT_IMPLEMENTED"]
        }
    @staticmethod
    def assignment(name, value):
        return name + "=" + value + "\n"
    @staticmethod
    def target_variable_assignment(name, value):
        return Keywords.assignment(name.upper()+"_TARGET", value)
class Item(object):
    def __init__(self, line=None, content=[]):
        self.idx = line
        self.str = content
        for l in iter(self.str):
            l = l.strip()
    def append(self, line):
        self.str.append(line.strip())
    def extend(self, content):
        for l in content:
            self.append(l)
    def line(self):
        return self.idx
    def length(self):
        return len(self.str)
    def value(self):
        return "".join(self.str).replace("\n","").strip()
    def set_value(self, value):
        self.str = [ i.strip()+"\n" for i in value.split("\n") ]
    def include_line(self):
        return "include "+self.str[0]
    def variable_assignment(self, variable):
        if self.length() == 1:
            return ["{0:<24}{1}".format(variable+"=",self.str[0])]
        # Handle continuation lines
        lines = ["{0:<24}{1}".format(variable+"=",self.str[0][:-2])]
        for l in self.str[1:]:
            lines[-1] += "\\\n"
            lines.append("\t"+l[:-2])
        lines[-1] += "\n"
        return lines
    def target_definition(self, target):
        lines = ["{0:<24}{1}".format(target+":", self.str[0])]
        for l in self.str[1:]:
            lines.append("\t"+l)
        return lines
class Makefile(object):
    def __init__(self, path=None, debug=False):
        self.debug = debug
        self.path = path
        self.component = Component()
        self.includes = []
        self.variables = {}
        self.targets = {}
        makefile = os.path.join(path, 'Makefile')
        with open(makefile, 'r') as f:
            self.contents = f.readlines()
        self.update()
    def update(self, contents=None):
        self.includes = []
        self.variables = {}
        self.targets = {}
        if contents is not None:
            self.contents = contents
        # Construct list of keywords
        kw = Keywords()
        # Variable is set
        m = None
        # Target is set
        t = None
        # Rule definition
        d = None
        for idx, line in enumerate(self.contents):
            # Continuation of target line
            if t is not None:
                r = re.match(r"^[\s]*(.*)[\s]*([\\]?)[\s]*$", line)
                # Concatenate
                self.targets[t].str[0] += "\\".join(r.group(1))
                # Check for continuation or move to definition
                if not r.group(2):
                    d = t
                    t = None
                continue
            if d is not None:
                # Concatenate
                r = re.match(r"^[\t][\s]*(.*)[\s]*$", line)
                # End of definition
                if r is None:
                    d = None
                    continue
                self.targets[d].append(r.group(1))
            # Continuation line of variable
            if m is not None:
                r = re.match(r"^[\s]*(.*)[\s]*([\\]?)[\s]*$", line)
                self.variables[m].append(r.group(1))
                if not r.group(2):
                    m = None
                continue
            if re.match(r"^#", line):
                continue
            # Check for include
            r = re.match(r"^include[\s]+(.*)", line)
            if r is not None:
               self.includes.append(Item(idx, [r.group(1)]))
            else:
                found = False
                # Collect known variables
                for k in list(kw.variables.keys()):
                    r = re.match(
                        r"^[\s]*("+k+r")[\s]*=[\s]*([^\\]*)[\s]*([\\]?)[\s]*$", line)
                    if r is not None:
                        found = True
                        v = r.group(2)
                        if v in self.variables.keys():
                            warnings.warn("Variable '"+v+"' redefined line "+idx)
                        self.variables[k] = Item(idx, [v])
                        if r.group(3):
                            m = k
                        break
                if found is True:
                    continue
                # Collect known targets
                for k in list(kw.targets.keys()):
                    r = re.match(
                        "^"+k+r"[\s]*:[\s]*(.*)[\s]*([\\]?)[\s]*$", line)
                    if r is not None:
                        found = True
                        v = r.group(1)
                        if v in self.targets.keys():
                            warnings.warn("Target '"+v+"' redefined line "+idx)
                        self.targets[k] = Item(idx, [v])
                        if r.group(2):
                            t = k
                            d = None
                        else:
                            t = None
                            d = k
                        break
                if found is True:
                    continue
    def write(self):
        with open(os.path.join(self.path, "Makefile"), 'w') as f:
            for line in self.contents:
                f.write(line)
    def display(self):
        print(self.path)
        print('-' * 78)
        if self.includes:
            print("includes:")
            print("---------")
            for i in iter(self.includes):
                print("{0:>3}: {1}".format(i.line(), i.value()))
            print("")
        if self.variables:
            print("variables:")
            print("----------")
            for k,i in iter(sorted(self.variables.items())):
                print("{0:>3}: {1:<24}= {2}".format(i.line(), k, i.value()))
            print("")
        if self.targets:
            print("targets:")
            print("--------")
            for k,i in iter(sorted(self.targets.items())):
                print("{0:>3}: {1:<24}= {2}".format(i.line(), k, i.value()))
            print("")
        print('-' * 78)
    def run(self, targets):
        path = self.path
        result = []
        if self.debug:
            logger.debug('Executing \'gmake %s\' in %s', targets, path)
        proc = subprocess.Popen(['gmake', '-s', targets],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                cwd=path,
                                universal_newlines=True)
        stdout, stderr = proc.communicate()
        for out in stdout.splitlines():
            result.append(out.rstrip())
        if self.debug:
            if proc.returncode != 0:
                logger.debug('exit: %d, %s', proc.returncode, stderr)
        return result
    def has_variable(self, variable):
        return variable in self.variables
    def variable(self, variable):
        return self.variables[variable]
    def remove_variable(self, variable):
        idx = self.variable(variable).line()
        del self.contents[idx]
        self.update()
    def set_variable(self, variable, value, line=None):
        if not self.has_variable(variable):
            self.variables[variable] = Item(None)
            self.variables[variable].set_value(str(value))
            if line is not None:
                contents = self.contents[0:line]
                contents.extend(self.variable_assignment(variable))
                contents.extend(self.contents[line:])
                self.update(contents)
            return True
        else:
            idx = self.variables[variable].line()
            oln = self.variables[variable].length()
            self.variables[variable].set_value(str(value))
            nln = self.variables[variable].length()
            if idx is not None:
                if line is None:
                    line = idx
                if line == idx and nln == 1 and oln == 1:
                    self.contents[idx] = self.variable_assignment(variable)[0]
                elif line <= idx:
                    contents = self.contents[0:line]
                    contents.extend(self.variable_assignment(variable))
                    contents.extend(self.contents[line:idx])
                    contents.extend(self.contents[idx+oln:])
                    self.update(contents)
                else:
                    contents = self.contents[0:idx]
                    contents.extend(self.contents[idx+oln:line])
                    contents.extend(self.variable_assignment(variable))
                    contents.extend(self.contents[line:])
                    self.update(contents)
            # Add variable at given line
            elif line is not None:
                contents = self.contents[0:line]
                contents.extend(self.variable_assignment(variable))
                contents.extend(self.contents[line:])
                self.update(contents)
    def variable_assignment(self, variable):
        return self.variables[variable].variable_assignment(variable)
    def has_target(self, target):
        return target in self.targets
    def target(self, target):
        return self.targets[target]
    def target_definition(self, target):
        return self.targets[target].target_definition(target)
    @staticmethod
    def value(variable):
        return "$("+variable+")"
    @staticmethod
    def directory_variable(subdir):
        return self.value("WS_"+subdir.upper().replace("-","_"))
    @staticmethod
    def makefile_path(name):
        return os.path.join("$(WS_MAKE_RULES)", name+".mk")
    @staticmethod
    def target_value(name, bits):
        return Makefile.value(name.upper()+"_"+bits)
tools/userland-component
New file
@@ -0,0 +1,234 @@
#!/usr/bin/python3.5
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source.  A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#
#
# Copyright 2021 Aurelien Larcher
#
import argparse
import os
import re
import sys
import json
from bass.component import Component
from bass.makefiles import Item
from bass.makefiles import Keywords
from bass.makefiles import Makefile as MK
# Refactoring rules
#-----------------------------------------------------------------------------
# They should be called in-order to avoid unsatisfied assumptions.
def format_component(path, verbose):
    mk = MK(path)
    kw = Keywords()
    refactor000(mk)
    refactor001(mk)
    refactor002(mk)
    mk.write()
#-----------------------------------------------------------------------------
# 000:  Use WS_* variables instead $(WS_TOP)/*
#       If $(WS_TOP)/make-rules is found in an include then replace with the
#       variable $(WS_RULES). Do the same for other variables.
def refactor000(mk):
    for i in iter(mk.includes):
        r = re.match(r"^\$\(WS_TOP\)\/(.*)\/(.*).mk", i.str[0])
        if r is not None:
            subdir = r.group(1)
            mkfile = r.group(2)
            print("000: Fix include " + i.str[0])
            i.set_value(os.path.join(MK.directory_variable(subdir), mkfile+".mk"))
            mk.contents[i.line()] = i.include_line()
#-----------------------------------------------------------------------------
# 001:  Use common.mk
#       If common.mk is not included then:
#           1. infer the build system and set the BUILD_STYLE.
#           2. set the BUILD_BITS from the existing targets.
#           3. erase default target and keep the custom ones.
def refactor001(mk):
    kw = Keywords()
    if mk.has_variable('BUILD_STYLE'):
        return
    # Build style
    build_style = None
    for i in iter(mk.includes):
        r = re.match(r"^\$\(WS_MAKE_RULES\)/(.*).mk$", i.value())
        if r is not None:
            build_style = r.group(1) if r.group(1) in kw.variables['BUILD_STYLE'] else None
            if build_style is not None:
                mk.set_variable('BUILD_STYLE', build_style)
                break
    if build_style is None:
        raise ValueError("Variable BUILD_STYLE cannot be defined")
    else:
        print("001: Setting build style to '" + build_style + "'")
    build_style = mk.variable('BUILD_STYLE').value()
    # Build bits
    mk_bits = mk.run("print-value-MK_BITS")[0]
    if mk_bits not in kw.variables["MK_BITS"]:
        raise ValueError("Variable MK_BITS cannot be defined")
    else:
        print("001: Setting make bits to '" + mk_bits + "'")
    # Check targets
    mk_bits_32_no_arch = False
    new_targets = {}
    for t, u in iter(mk.targets.items()):
        # We do not know how to handle target with defined steps yet
        if len(u.str) > 1:
            continue
        # Process target
        found = False
        for v in kw.targets[t]:
            v = MK.value(v.replace(MK.value("MK_BITS"), mk_bits))
            # If the target dependency is one of the default values
            if u.str[0] == v:
                found = True
                w = MK.target_value(t, mk_bits)
                if v == w:
                    print("001: Use default target '"+t+"'")
                    u.str = None
                else:
                    print("001: Define target '"+t+"': "+u.str[0])
                    new_targets[t] = u
                break
        if not found:
            # Some Python/Perl makefiles actually use NO_ARCH target with MK_BITS=32
            if mk_bits == '32' and u.str[0] == MK.value(t.upper()+"_NO_ARCH"):
                if not mk_bits_32_no_arch:
                    print("001: Changing make bits from '32' to 'NO_ARCH'")
                    mk_bits_32_no_arch = True
                u.str = None
            else:
                raise ValueError("001: Inconsistent target '"+t+"': "+u.str[0])
    if mk_bits_32_no_arch:
        mk_bits = "NO_ARCH"
    # Collect items
    rem_lines = set()
    rem_includes = [ MK.makefile_path("prep"), MK.makefile_path("ips")]
    new_includes = []
    include_shared_mk = None
    include_common_mk = None
    for i in iter(mk.includes):
        if i.value() not in rem_includes:
            if i.value() == MK.makefile_path(build_style):
                i.set_value(MK.makefile_path("common"))
                include_common_mk = i
            elif re.match(r".*/shared-macros.mk$", i.str[0]):
                include_shared_mk = i
            new_includes.append(i)
        else:
            rem_lines.add(i.line())
    mk.includes = new_includes
    if include_common_mk is None:
        raise ValueError("Include directive of common.mk not found")
    if include_shared_mk is None:
        raise ValueError("Include directive of shared-macros.mk not found")
    # Add lines to skip for default targets
    for u in mk.targets.values():
        if u.str is None:
            rem_lines.add(u.line())
    # Update content
    contents = mk.contents[0:include_shared_mk.line()]
    # Add build macros
    contents.append(Keywords.assignment('BUILD_STYLE', build_style))
    contents.append(Keywords.assignment('BUILD_BITS', mk_bits))
    # Write metadata lines
    for idx, line in enumerate(mk.contents[include_shared_mk.line():include_common_mk.line()]):
        if (include_shared_mk.line() + idx) in rem_lines:
            continue
        contents.append(line)
    # Write new targets
    for t  in ["build", "install", "test"]:
        if t in new_targets.keys():
            contents.append(Keywords.target_variable_assignment(t, new_targets[t].str[0]))
            rem_lines.add(new_targets[t].line())
    # Add common include
    contents.append(include_common_mk.include_line())
    # Write lines after common.mk
    for idx, line in enumerate(mk.contents[include_common_mk.line()+1:]):
        if (include_common_mk.line()+1+idx) in rem_lines:
            continue
        contents.append(line)
    mk.update(contents)
#-----------------------------------------------------------------------------
# 002:  Indent COMPONENT_ variables
def refactor002(mk):
    for k,i in iter(mk.variables.items()):
        if re.match("^COMPONENT_", k):
            idx = i.line()
            lines = i.variable_assignment(k)
            for i in range(0, i.length()):
                mk.contents[idx + i] = lines[i]
    mk.update()
#-----------------------------------------------------------------------------
# Update component makefile for revision or version bump
def update_component(path, version, verbose):
    format_component(path, verbose)
    # Nothing to bump, just update the Makefile to current format
    if version is None:
        return
    mk = MK(path)
    if not mk.has_variable('COMPONENT_VERSION'):
        raise ValueError('COMPONENT_VERSION not found')
    newvers = str(version)
    current = mk.variable('COMPONENT_VERSION').value()
    # Bump revision only
    if newvers == '0' or newvers == current:
        print("Bump COMPONENT_REVISION")
        if mk.has_variable('COMPONENT_REVISION'):
            try:
                component_revision = int(mk.variable('COMPONENT_REVISION').value())
            except ValueError:
                print('COMPONENT_REVISION field malformed: {}'.format(component_revision))
            # Change value
            mk.set_variable('COMPONENT_REVISION', str(component_revision+1))
        else:
            # Add value set to 1 after COMPONENT_VERSION
            mk.set_variable('COMPONENT_REVISION', str(1), line=mk.variable('COMPONENT_VERSION').line()+1)
    # Update to given version and remove revision
    else:
        print("Bump COMPONENT_VERSION to " + newvers)
        mk.set_variable('COMPONENT_VERSION', newvers)
        if mk.has_variable('COMPONENT_REVISION'):
            mk.remove_variable('COMPONENT_REVISION')
    # Update makefile
    mk.write()
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--path', default='components',
                        help='Directory holding components')
    parser.add_argument('--bump', nargs='?', default=None, const=0,
                        help='Bump component to given version')
    parser.add_argument('-v', '--verbose', action='store_true',
                        default=False, help='Verbose output')
    args = parser.parse_args()
    path = args.path
    version = args.bump
    verbose = args.verbose
    update_component(path=path, version=version, verbose=verbose)
if __name__ == '__main__':
    main()