488 строки
19 KiB
Python
488 строки
19 KiB
Python
import sys
|
|
import re
|
|
import os
|
|
|
|
# Valid sections and the sets they belong to
|
|
FILE_SECTIONS = [ "Files", "Directories", "Links" ]
|
|
SCRIPT_SECTIONS = [ "Preinstall", "Postinstall", "Preuninstall", "Postuninstall", "iConfig", "rConfig", "Preupgrade" ]
|
|
DEPENDENCY_SECTIONS = [ "Dependencies" ]
|
|
VAR_SECTIONS = [ "Variables", "Defines" ]
|
|
|
|
def error(s):
|
|
sys.stderr.write("Internal Error: %s \n" % s)
|
|
exit(1)
|
|
|
|
def error(s, line):
|
|
sys.stderr.write("Error[%s:%s]: \n" % (line[1], line[2]) + s)
|
|
exit(1)
|
|
|
|
def error_section(s, section):
|
|
sys.stderr.write("Error[%s]: " % section + s)
|
|
exit(1)
|
|
|
|
def warning(s, line):
|
|
sys.stderr.write("Warning[%s:%s]: \n" % (line[1], line[2]) + s)
|
|
|
|
def info(s, line):
|
|
sys.stderr.write("Info[%s:%s]: \n" % (line[1], line[2]) + s)
|
|
|
|
def invalid_varname(s):
|
|
if " " in s:
|
|
return True
|
|
return False
|
|
|
|
def CheckIfCommand(s):
|
|
if s in ["#if", "#ifdef", "#ifndef", "#elseif", "#else", "#elseifdef", "#endif", "#include"]:
|
|
return True
|
|
return False
|
|
|
|
variable_usage = "Invalid variable line entry. Usage: VARIABLE_NAME: 'VALUE'"
|
|
define_usage = "Invalid define line entry. Usage: DEFINE_NAME"
|
|
too_many_ifs = "There is at least one open conditional (#if) that has not been closed by the end of this section"
|
|
too_many_endifs = "There is at least one extra end conditional (#endif) in this section."
|
|
|
|
def edge_quotes_match(s):
|
|
if len(s) < 2:
|
|
return False
|
|
if s[0] == "'" and s[-1] == "'":
|
|
return True
|
|
if s[0] == '"' and s[-1] == '"':
|
|
return True
|
|
return False
|
|
|
|
# FileEntry
|
|
# param: tokens - a list containing [ stagedLocation, baseLocation, permissions, owner, group (, type) ]
|
|
# which correspond exactly with a line in a Files section (the tokens separated by semicolons)
|
|
class FileEntry:
|
|
def __init__(self, tokens, line):
|
|
if len(tokens) < 5 or len(tokens) > 6:
|
|
error("Incorrect number of tokens in File entry", line)
|
|
|
|
self.stagedLocation = tokens[0]
|
|
self.baseLocation = tokens[1]
|
|
self.permissions = tokens[2]
|
|
self.owner = tokens[3]
|
|
self.group = tokens[4]
|
|
if len(tokens) == 6:
|
|
self.type = tokens[5]
|
|
else:
|
|
self.type = ""
|
|
|
|
def __str__(self):
|
|
return self.stagedLocation + "; " + self.baseLocation + "; " + self.permissions + "; " + self.owner + "; " + self.group + "; " + self.type
|
|
|
|
# LinkEntry
|
|
# param: tokens - a list containing [ stagedLocation, baseLocation, permissions, owner, group ]
|
|
# which correspond exactly with a line in a Links section (the tokens separated by semicolons)
|
|
class LinkEntry:
|
|
def __init__(self, tokens, line):
|
|
if len(tokens) != 5:
|
|
error("Incorrect number of tokens in Link entry", line)
|
|
|
|
self.stagedLocation = tokens[0]
|
|
self.baseLocation = tokens[1]
|
|
self.permissions = tokens[2]
|
|
self.owner = tokens[3]
|
|
self.group = tokens[4]
|
|
self.type = ""
|
|
|
|
def __str__(self):
|
|
return self.stagedLocation + "; " + self.baseLocation + "; " + self.permissions + "; " + self.owner + "; " + self.group
|
|
|
|
# DirectoryEntry
|
|
# param: tokens - a list containing [ stagedLocation, permissions, owner, group (, type) ]
|
|
# which correspond exactly with a line in a Directories section (the tokens separated by semicolons)
|
|
class DirectoryEntry:
|
|
def __init__(self, tokens, line):
|
|
if len(tokens) < 4 or len(tokens) > 5:
|
|
error("Incorrect number of tokens in Directory entry", line)
|
|
|
|
self.stagedLocation = tokens[0]
|
|
self.permissions = tokens[1]
|
|
self.owner = tokens[2]
|
|
self.group = tokens[3]
|
|
if len(tokens) == 5:
|
|
self.type = tokens[4]
|
|
else:
|
|
self.type = ""
|
|
|
|
def __str__(self):
|
|
return self.stagedLocation + "; " + self.permissions + "; " + self.owner + "; " + self.group + "; " + self.type
|
|
|
|
# ConditionalLevel
|
|
# param: has_executed - whether this conditional level has evaluated to true before
|
|
# param: currently_executing - whether this level is currently executing
|
|
class ConditionalLevel:
|
|
def __init__(self, has_executed, currently_executing):
|
|
self.has_executed = has_executed
|
|
self.currently_executing = currently_executing
|
|
|
|
# ConditionalStack
|
|
# Description:
|
|
# This stack is the data structure used to keep track of the many levels of conditionals that can exist.
|
|
class ConditionalStack:
|
|
def __init__(self):
|
|
self.level_stack = []
|
|
|
|
def IsCodePathActive(self):
|
|
for level in self.level_stack:
|
|
if not level.has_executed:
|
|
return False
|
|
if not level.currently_executing:
|
|
return False
|
|
return True
|
|
|
|
# Adds a new level to the conditional stack. This occurs when there's a #if*.
|
|
def AddLevel(self):
|
|
self.level_stack.append(ConditionalLevel(False, False))
|
|
|
|
# Removes a level from the conditional stack. This occurs when there's a #endif
|
|
def RemoveLevel(self):
|
|
if len(self.level_stack) == 0:
|
|
error("Cannot RemoveLevel, there are no levels on the ConditionalStack")
|
|
|
|
self.level_stack.pop()
|
|
|
|
# This notifies the conditional stack that we want to execute the current level of the stack.
|
|
def ExecuteCurrentLevel(self):
|
|
level = self.level_stack.pop()
|
|
if level.has_executed == True or level.currently_executing == True:
|
|
error("Trying to execute a conditional level that has already been executed or is currently executing.")
|
|
self.level_stack.append(ConditionalLevel(True, True))
|
|
|
|
# This is called in an #else* clause. This is to turn off the "currently_executing" flag if it is true.
|
|
def NextConditional(self):
|
|
# End execution if currently_executing
|
|
level = self.level_stack.pop()
|
|
if level.currently_executing == True:
|
|
level.currently_executing = False
|
|
self.level_stack.append(level)
|
|
|
|
# This is called in an #else* clause.
|
|
# returns - True if the current level has not been executed yet, False otherwise.
|
|
def CurrentLevelHasNotBeenExecutedYet(self):
|
|
if self.Empty():
|
|
error("Expecting ConditionalStack to have at least one level in it")
|
|
|
|
return not self.level_stack[-1].has_executed
|
|
|
|
def Empty(self):
|
|
return len(self.level_stack) == 0
|
|
|
|
# DataFileParser
|
|
# Description:
|
|
# This class reads the datafiles, parses them, and evaluates the commands.
|
|
class DataFileParser:
|
|
def __init__(self):
|
|
self.variables = dict()
|
|
self.defines = []
|
|
|
|
# Used for debugging
|
|
def PrintSections(self):
|
|
sorted_keys = sorted(self.sections.keys())
|
|
for key in sorted_keys:
|
|
print("****************************************** " + key + " ******************************************")
|
|
for line in self.sections[key]:
|
|
print(str(line))
|
|
|
|
# This function handles all commands. Conditionals will be evaluted to either True and False and the conditional stack will be updated.
|
|
# Includes will insert the included section into the including section (which is variable name 'section').
|
|
# param: line - The command to be evaluated.
|
|
# param: ifstack - This is the class' ConditionalStack/
|
|
# param: section - The entire section currently being processed.
|
|
# param: linenum - Used for error messages.
|
|
# returns: 1. ifstack
|
|
# 2. section (modified if there is an #include)
|
|
# 3. linenum (modified if there is an #include)
|
|
def HandleCommand(self, line, ifstack, section, linenum):
|
|
commandline = line[0]
|
|
if len(commandline) == 0:
|
|
return ifstack
|
|
|
|
tokens = commandline.split()
|
|
firsttoken = tokens[0]
|
|
|
|
if firsttoken == "#if":
|
|
ifstack.AddLevel()
|
|
if self.Evaluate(tokens[1:], line) == True:
|
|
ifstack.ExecuteCurrentLevel()
|
|
elif firsttoken == "#ifdef":
|
|
ifstack.AddLevel()
|
|
if self.IsDefined(tokens[1:], line) == True:
|
|
ifstack.ExecuteCurrentLevel()
|
|
elif firsttoken == "#ifndef":
|
|
ifstack.AddLevel()
|
|
if self.IsDefined(tokens[1:], line) == False:
|
|
ifstack.ExecuteCurrentLevel()
|
|
|
|
elif firsttoken == "#elseif":
|
|
ifstack.NextConditional()
|
|
|
|
if ifstack.CurrentLevelHasNotBeenExecutedYet() and self.Evaluate(tokens[1:], line) == True:
|
|
ifstack.ExecuteCurrentLevel()
|
|
elif firsttoken == "#elseifdef":
|
|
ifstack.NextConditional()
|
|
|
|
if ifstack.CurrentLevelHasNotBeenExecutedYet() and self.IsDefined(tokens[1:], line) == True:
|
|
ifstack.ExecuteCurrentLevel()
|
|
|
|
elif firsttoken == "#else":
|
|
ifstack.NextConditional()
|
|
|
|
if ifstack.CurrentLevelHasNotBeenExecutedYet():
|
|
ifstack.ExecuteCurrentLevel()
|
|
|
|
elif firsttoken == "#endif":
|
|
ifstack.RemoveLevel()
|
|
|
|
elif firsttoken == "#include":
|
|
middle_section = []
|
|
for L in self.EvaluateSection(tokens[1]):
|
|
middle_section.append(L)
|
|
section = section[0:linenum-1] + middle_section + section[linenum-1:]
|
|
linenum += len(middle_section)
|
|
return ifstack, section, linenum
|
|
|
|
# Expression evaluator. Returns True if expression is True, False otherwise.
|
|
def Evaluate(self, expressions, line):
|
|
expr = expressions
|
|
if len(expr) < 3:
|
|
error("Bad syntax for #if")
|
|
|
|
# This will be in the format of: #if VAR OP VALUE (and/or VAR OP VALUE)*
|
|
var = expr[0]
|
|
op = expr[1]
|
|
value = expr[2]
|
|
|
|
if self.variables.get(var) == None:
|
|
error("Unable to find variable " + var + " in defined variables", line)
|
|
|
|
if op == "==":
|
|
return self.variables[var] == value
|
|
elif op == "!=":
|
|
return self.variables[var] != value
|
|
elif op == ">":
|
|
return self.variables[var] > float(value)
|
|
elif op == ">=":
|
|
return self.variables[var] >= float(value)
|
|
elif op == "<":
|
|
return self.variables[var] < float(value)
|
|
elif op == "<=":
|
|
return self.variables[var] <= float(value)
|
|
else:
|
|
error("operator %s is not valid" % op, line);
|
|
|
|
# Returns True if the expression exists in the defines or variables, otherwise False.
|
|
def IsDefined(self, expressions, line):
|
|
expr = expressions
|
|
if len(expr) < 1:
|
|
error("Bad syntax for #ifdef", line)
|
|
|
|
var = expr[0]
|
|
if var in self.defines:
|
|
return True
|
|
if var in self.variables.keys():
|
|
return True
|
|
return False
|
|
|
|
# This is called on every line that isn't in the Variables or Defines sections, and it replaces any text inside
|
|
# two sets of braces starting with a dollar sign with the value in the self.variables dict. "${{VAR_NAME}}"
|
|
def ReplaceVariables(self, line):
|
|
# replaces all variables that appear in the form of ${{\w+}}
|
|
var_rex = re.compile(r"\$\{\{\w+\}\}")
|
|
for m in var_rex.findall(line):
|
|
line = line.replace(m, self.variables[m[3:-2]])
|
|
return line
|
|
|
|
# This function combines, in numeric order, all of the script sections for a given 'name' (ex. "Preinstall") for all numeric values appended to the 'name'.
|
|
# So if there are sections named Preinstall_10 and Preinstall_50 and Preinstall_3, this function when called for name="Preinstall" will return a section
|
|
# that contains all of the lines in Preinstall_3, followed by all of the lines in Preinstall_10, followed by all of the lines in Preinstall_50.
|
|
def GetCombinedInOrder(self, name):
|
|
returnList = []
|
|
orderedSectionList = []
|
|
|
|
# Find all sections that match 'name' followed by some optional underscores, followed by any combination of digits,
|
|
# then store those digits for numeric sorting and combining scripts
|
|
for section in self.sections.keys():
|
|
section_rex = re.compile(r"%s_*(\d*)" % name)
|
|
m = section_rex.match(section)
|
|
if m != None:
|
|
sectionPriority = m.group(1)
|
|
if sectionPriority == "":
|
|
sectionPriority = "0"
|
|
orderedSectionList.append( (int(sectionPriority), section) )
|
|
|
|
orderedSectionList.sort()
|
|
|
|
for orderedSection in orderedSectionList:
|
|
for line in self.sections[ orderedSection[1] ]:
|
|
returnList.append(line)
|
|
|
|
return returnList
|
|
|
|
# Reads all lines in each datafile and inserts all lines from each section into self.sections.
|
|
def InhaleDataFiles(self, directory, filenames):
|
|
sections = dict()
|
|
for filename in filenames:
|
|
state = None
|
|
|
|
f = open(os.path.join(directory,filename))
|
|
lines = f.readlines()
|
|
f.close()
|
|
|
|
linenumber = 0
|
|
for line in lines:
|
|
linenumber += 1
|
|
line = line.strip("\n")
|
|
|
|
if len(line) > 0 and line[0] == '%':
|
|
if len(line) > 1 and line[1] == '%':
|
|
# This is a comment, ignore it
|
|
continue
|
|
# Begin new section
|
|
state = line[1:]
|
|
if sections.get(state) == None:
|
|
sections[state] = []
|
|
elif state not in FILE_SECTIONS + DEPENDENCY_SECTIONS + VAR_SECTIONS:
|
|
error_section("This script section is defined more than once.", state)
|
|
|
|
else:
|
|
# Handle states
|
|
sections[state].append( (line, filename, linenumber) )
|
|
|
|
self.sections = sections
|
|
|
|
# This function is called after the datafiles have been inhaled, and it parses and handles each entry in the Variables and Defines sections.
|
|
# This allows for later sections to then rely on variables referenced by ${{VAR_NAME}}.
|
|
def EvaluateVariablesAndDefines(self):
|
|
# Add variables/defines from data files
|
|
if "Variables" in self.sections:
|
|
ifstack = ConditionalStack()
|
|
for line in self.sections["Variables"]:
|
|
varline = line[0].strip()
|
|
if len(varline) == 0:
|
|
continue
|
|
|
|
if CheckIfCommand(varline.split(" ", 1)[0]):
|
|
ifstack, dummy, dummy = self.HandleCommand(line, ifstack, [], 0)
|
|
continue
|
|
|
|
# If we're currently in a conditional part of the data file that evaluates to false
|
|
if not ifstack.IsCodePathActive():
|
|
continue
|
|
|
|
tokens = varline.split(":", 1)
|
|
|
|
if len(tokens) != 2:
|
|
error(variable_usage, line)
|
|
|
|
key = tokens[0].strip()
|
|
value = tokens[1].strip()
|
|
|
|
if len(value) < 2 or not edge_quotes_match(value):
|
|
error(variable_usage, line)
|
|
|
|
# remove the quotes around the variable value
|
|
value = value[1:-1]
|
|
if self.variables.get(key) != None:
|
|
info("Variable %s is already defined." % key, line)
|
|
|
|
# add the parsed variable to the variables dict
|
|
self.variables[key] = value
|
|
|
|
if not ifstack.Empty():
|
|
error_section(too_many_ifs, "Variables")
|
|
|
|
if "Defines" in self.sections:
|
|
ifstack = ConditionalStack()
|
|
for line in self.sections["Defines"]:
|
|
defline = line[0].strip()
|
|
if len(defline) == 0:
|
|
continue
|
|
|
|
if CheckIfCommand(defline.split(" ", 1)[0]):
|
|
ifstack, dummy, dummy = self.HandleCommand(line, ifstack, [], 0)
|
|
continue
|
|
|
|
if not ifstack.IsCodePathActive():
|
|
continue
|
|
|
|
if invalid_varname(defline):
|
|
error(define_usage, line)
|
|
|
|
if defline in self.defines:
|
|
info("Define %s has already been defined." % defline, line)
|
|
|
|
# add the define to the define list
|
|
self.defines.append(defline)
|
|
|
|
if not ifstack.Empty():
|
|
error_section(too_many_ifs, "Defines")
|
|
|
|
# This evaluates all commands in a given section denoted by 'section', then returns the evaluated lines for the section.
|
|
def EvaluateSection(self, section):
|
|
newsection = []
|
|
ifstack = ConditionalStack()
|
|
|
|
if section in SCRIPT_SECTIONS:
|
|
# Combine and evaluate each script section
|
|
cursection = self.GetCombinedInOrder(section)
|
|
else:
|
|
cursection = self.sections[section]
|
|
|
|
linenum = 0
|
|
for line in cursection:
|
|
linenum += 1
|
|
line_literal = line[0]
|
|
if section in FILE_SECTIONS + DEPENDENCY_SECTIONS + VAR_SECTIONS:
|
|
if len(line_literal.strip()) == 0:
|
|
continue
|
|
|
|
if CheckIfCommand(line_literal.split(" ", 1)[0]):
|
|
ifstack, newsection, linenum = self.HandleCommand(line, ifstack, newsection, linenum)
|
|
continue
|
|
|
|
if not ifstack.IsCodePathActive():
|
|
continue
|
|
|
|
line_literal = self.ReplaceVariables(line_literal)
|
|
if section in FILE_SECTIONS:
|
|
tokens = line_literal.split(";")
|
|
newtokens = []
|
|
for token in tokens:
|
|
newtokens.append(token.strip())
|
|
|
|
if section == "Files":
|
|
newsection.append(FileEntry(newtokens, line))
|
|
elif section == "Directories":
|
|
newsection.append(DirectoryEntry(newtokens, line))
|
|
elif section == "Links":
|
|
newsection.append(LinkEntry(newtokens, line))
|
|
else:
|
|
error("Failing to parse line type in '" + section + "'.", line)
|
|
else:
|
|
newsection.append(line_literal)
|
|
|
|
if not ifstack.Empty():
|
|
error_section(too_many_ifs, section)
|
|
|
|
return newsection
|
|
|
|
# This calls EvaluateSection on each "Base section" as mentioned in the README.
|
|
def EvaluateAllSections(self):
|
|
# Read through each line, evaluating #if's/#define's, evaluating variables, tokenizing/classing relevant lines, adding to new sections list
|
|
newsections = dict()
|
|
section_keys = FILE_SECTIONS + SCRIPT_SECTIONS + DEPENDENCY_SECTIONS
|
|
for section in section_keys:
|
|
newsections[section] = self.EvaluateSection(section)
|
|
|
|
self.sections = newsections
|
|
|
|
# Used for debugging.
|
|
def Debug(self):
|
|
self.PrintSections()
|
|
print("****************************** Variables ******************************")
|
|
print(self.variables)
|
|
print("****************************** Defines ******************************")
|
|
print(self.defines)
|