#!/usr/bin/python3.9 # # 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 json import os import re import subprocess import warnings from urllib.request import urlopen 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])] for l in self.str[1:]: lines[-1] += "\\\n" lines.append("\t"+l) 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 print_value(self, name): return self.run('print-value-'+name)[0] def build_style(self): return self.variables['BUILD_STYLE'].value() def build_bits(self): return self.variables['BUILD_BITS'].value() def has_mk_include(self, name): for i in iter(self.includes): if re.match('^.*/'+name+'.mk$', i.value()): return True return False def get_mk_include(self, name): for i in iter(self.includes): if re.match('^.*/'+name+'.mk$', i.value()): return i return None 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 set_archive_hash(self, checksum): self.set_variable('COMPONENT_ARCHIVE_HASH', "sha256:"+str(checksum)) 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) def required_packages(self): return self.run("print-value-REQUIRED_PACKAGES")[0] def uses_pypi(self): # Default build style is configure if not self.has_variable('BUILD_STYLE'): return False is_py = (self.build_style() == 'setup.py') urlnone = (not self.has_variable('COMPONENT_ARCHIVE_URL')) urlpipy = urlnone or (self.variable('COMPONENT_ARCHIVE_URL').value() == '$(call pypi_url)') return is_py and urlpipy def get_pypi_data(self): name = self.print_value('COMPONENT_PYPI') jsurl = "https://pypi.python.org/pypi/%s/json" % name try: f = urlopen(jsurl, data=None) except HTTPError as e: if e.getcode() == 404: print("Unknown component '%s'" % name) else: printIOError(e, "Can't open PyPI JSON url %s" % jsurl) return None except IOError as e: printIOError(e, "Can't open PyPI JSON url %s" % jsurl) return None content = f.read().decode("utf-8") return json.loads(content) @staticmethod def value(variable): return "$("+variable+")" @staticmethod def directory_variable(subdir): return Makefile.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)