build_ninja.py   [plain text]


#!/usr/bin/python2.7

import plistlib
import string
import argparse
import sys
import os
import tempfile
import shutil
import subprocess
import re
import hashlib
import textwrap
from string import Template

class BufferedFile:
    def __init__(self, fileName):
        self.data = ""
    	self.fileName = os.path.abspath(fileName)
    def __enter__(self):
        return self
    def __exit__(self, type, value, traceback):
        if os.path.exists(self.fileName):
            with open(self.fileName, "r") as file:
                fileData = file.read()
                if fileData == self.data: return
        else:
            dir = os.path.dirname(self.fileName)
            if not os.path.exists(dir):
                os.makedirs(dir)
        with open(self.fileName, "w") as file:
            file.write(self.data)
    def write(self, str):
        self.data += str

class NinjaFile:
    class Variable:
        def __init__(self, name, value):
            self.name = name
            self.value = value
        def __lt__(self, other):
            return self.name.__lt__(other.name)
        def __str__(self):
            return NinjaFile.lineWrap("{} = {}".format(self.name, self.value))
    
    class Rule:
        def __init__(self, name, command, depfile):
            self.name = name
            self.command = command
            self.depfile = depfile
        def __lt__(self, other):
            return self.name.__lt__(other.name)
        def __str__(self):
            result = NinjaFile.lineWrap("rule {}".format(self.name))
            if self.command: result += ("\n"+ NinjaFile.lineWrap("    command = {}".format(self.command)))
            if self.depfile:
                result += ("\n" + NinjaFile.lineWrap("    deps = gcc"))
                result += ("\n" + NinjaFile.lineWrap("    depfile = {}".format(self.depfile)))
            return result
    class Target:
        def __init__(self, rule):
            self.rule = rule
            self.output = ""
            self.inputs = []
            self.variables = []
            self.dependencies = []
        def __lt__(self, other):
            return self.output.__lt__(other.output)
        def __str__(self):
            self.inputs.sort()
            self.variables.sort()
            self.dependencies.sort()
            buildLine = "build {}: {}".format(self.output, self.rule)
            if self.inputs: buildLine += " {}".format(" ".join(self.inputs))
            if self.dependencies: buildLine += " | {}".format(" ".join(self.dependencies))
            result = NinjaFile.lineWrap(buildLine)
            for variable in self.variables: result += ("\n" + NinjaFile.lineWrap("    " + str(variable)))
            return result
        def addVariable(self, name, value): self.variables.append(NinjaFile.Variable(name, value))
        def addDependency(self, dependency):
            if isinstance(dependency, str):
                self.dependencies.append(dependency)
            elif isinstance(dependency, NinjaFile.Target):
                self.dependencies.append(dependency.output)
            else:
                raise ValueError("dependency must be a string or NinjaFile.Target")
        def addInput(self, input):
            if isinstance(input, str):
                self.inputs.append(input)
            elif isinstance(input, NinjaFile.Target):
                self.inputs.append(input.output)
            else:
                raise ValueError("input must be a string or NinjaFile.Target")
    class Include:
        def __init__(self, file):
            self.file = file
        def __lt__(self, other):
            return self.file.__lt__(other.file)
        def __str__(self):
            return NinjaFile.lineWrap("include {}".format(self.file))

    def __init__(self, fileName):
        self.fileName = os.path.abspath(fileName)
        self.rules = []
        self.variables = []
        self.targets = []
        self.includes = []
    def __enter__(self):
        return self
    def __exit__(self, type, value, traceback):
        with BufferedFile(self.fileName) as file:
            file.write(str(self))
    def addRule(self, name, command, deps): self.rules.append(NinjaFile.Rule(name, command, deps))
    def addVariable(self, name, value): self.variables.append(NinjaFile.Variable(name, value))
    def addInclude(self, file): self.includes.append(NinjaFile.Include(file))
    def newTarget(self, type, name):
        target = NinjaFile.Target(type)
        target.output = name
        self.targets.append(target)
        return target
    def findTarget(self, name):
        #PERF If this gets to be significant we can sort the array and binary search it
        for target in self.targets:
            if target.output == name: return target
        raise ValueError("Target \"{}\"  not found".format(name))
    def deleteTarget(self, name): self.targets.remove(self.findTarget(name))
    def __str__(self):
        self.variables.sort()
        self.rules.sort()
        self.targets.sort()
        self.includes.sort()
        subs = {
            "VARIABLES": "\n".join(map(str, self.variables)),
            "RULES": "\n\n".join(map(str, self.rules)),
            "TARGETS": "\n\n".join(map(str, self.targets)),
            "INCLUDES": "\n\n".join(map(str, self.includes))
        }
        return string.Template(
"""ninja_required_version = 1.6

$INCLUDES

$VARIABLES

$RULES

$TARGETS

""").safe_substitute(subs)
#    wrapper = textwrap.TextWrapper(width = 130, subsequent_indent = "    ", break_long_words = False)
    @classmethod
    def lineWrap(cls, line):
        if len(line) <= 132: return line
        result = ""
        currentIdx = 0
        wrappedLineLeadingSpace = "  "
        firstLineIndent = 0
        if line[0].isspace():
            firstLineIndent = 4
            result = "    "
            wrappedLineLeadingSpace = "      "
        trailer = " $"
        wrappedLineLeadingSpaceLen = len(wrappedLineLeadingSpace)
        lineSpaceAvailable = 132-(firstLineIndent+wrappedLineLeadingSpaceLen)
        words = line.split()
        wordsCount = len(words)-1
        for idx, word in enumerate(words):
            wordLen = len(word)
            if (wordLen <= lineSpaceAvailable and idx == wordsCount):
                result += word
            elif wordLen <= lineSpaceAvailable+2:
                result += "{} ".format(word)
                lineSpaceAvailable -= (wordLen)
            else:
                result += "$\n{}{} ".format(wrappedLineLeadingSpace, word)
                lineSpaceAvailable = 132-(wrappedLineLeadingSpaceLen+wordLen)
        return result

def processBuildLines(ninja, buildLines, testName, macOSBuild, minOS, testDstDir, testSrcDir):
    testInstallTarget = ninja.newTarget("phony", "install-{}".format(testName))
    testTarget = ninja.newTarget("phony", testName)
    ninja.findTarget("all").addInput(testTarget)
    ninja.findTarget("install").addInput(testInstallTarget)
    for buildLine in buildLines:
        args = buildLine.split()
        if args[0] == "$DTRACE":
            target = None
            for idx, arg in enumerate(args):
                if arg == "-o": target = ninja.newTarget("dtrace", args[idx+1])
            for idx, arg in enumerate(args):
                if arg == "-s": target.addInput(testSrcDir + "/" + args[idx+1])
        elif args[0] == "$CP":
            target = ninja.newTarget("cp", args[2])
            target.addInput(testSrcDir + "/" + args[1])
            testTarget.addInput(target)
            installTarget = ninja.newTarget("install", "$INSTALL_DIR/AppleInternal/CoreOS/tests/dyld/{}".format(args[2][9:]))
            installTarget.addInput(target.output)
            testInstallTarget.addInput(installTarget)
        elif args[0] == "$SYMLINK":
            target = ninja.newTarget("symlink", args[2])
            target.addVariable("source", args[1])
            testTarget.addInput(target)
            installTarget = ninja.newTarget("symlink", "$INSTALL_DIR/AppleInternal/CoreOS/tests/dyld/{}".format(args[2][9:]))
            installTarget.addVariable("source", args[1])
            testInstallTarget.addInput(installTarget)
        elif args[0] == "$STRIP":
            target = ninja.findTarget(args[1])
            target.addVariable("extraCmds", "&& strip {}".format(target.output))
        elif args[0] == "$SKIP_INSTALL":
            target = "$INSTALL_DIR/AppleInternal/CoreOS/tests/dyld/{}".format(args[1][9:])
            ninja.deleteTarget(target)
            testInstallTarget.inputs.remove(target)
        elif args[0] == "$DYLD_ENV_VARS_ENABLE":
            if not macOSBuild:
                target = ninja.findTarget(args[1])
                target.addVariable("entitlements", "--entitlements $SRCROOT/testing/get_task_allow_entitlement.plist")
        elif args[0] == "$TASK_FOR_PID_ENABLE":
            if not macOSBuild:
                target = ninja.findTarget(args[1])
                target.addVariable("entitlements", "--entitlements $SRCROOT/testing/task_for_pid_entitlement.plist")
        elif args[0] in ["$CC", "$CXX"]:
            tool = args[0][1:].lower()
            sources = []
            cflags = []
            ldflags = []
            dependencies = []
            skipCount = 0
            linkTarget = None
            isMainExecutable = True
            targetNames = [target.output for target in ninja.targets]
            args = [escapedArg.replace("\"", "\\\"") for escapedArg in args[1:]]
            #First find the target
            for idx, arg in enumerate(args):
                if arg == "-o":
                    linkTarget = ninja.newTarget("{}-link".format(tool), args[idx+1])
                    linkTarget.addDependency("$BUILT_PRODUCTS_DIR/libtest_support.a")
                    testTarget.addInput(linkTarget);
                    break
            skipCount = 0
            for idx, arg in enumerate(args):
                if skipCount: skipCount -= 1
                elif arg == "-o":
                    skipCount = 1
                elif arg == "$DEPENDS_ON":
                    skipCount = 1
                    dependencies.append(args[idx+1])
                elif arg in ["-arch"]:
                    skipCount = 1
                    nextArg = args[idx+1]
                    ldflags.append(arg)
                    ldflags.append(nextArg)
                    cflags.append(arg)
                    cflags.append(nextArg)
                elif arg in ["-install_name","-framework", "-rpath","-compatibility_version","-sub_library", "-undefined", "-current_version"]:
                    skipCount = 1
                    nextArg = args[idx+1]
                    ldflags.append(arg)
                    ldflags.append(nextArg)
                elif arg == "-sectcreate":
                    skipCount = 3
                    ldflags.append(arg)
                    ldflags.append(args[idx+1])
                    ldflags.append(args[idx+2])
                    ldflags.append(args[idx+3])
                elif arg[:2] == "-L": ldflags.append(arg)
                elif arg in ["-nostdlib", "-flat_namespace"]: ldflags.append(arg)
                elif arg in ["-dynamiclib","-bundle"]:
                    ldflags.append(arg)
                    isMainExecutable = False
                elif arg.endswith((".s", ".c", ".cpp", ".cxx", ".m", ".mm")):
                    sources.append(testSrcDir + "/" +arg)
                elif arg in targetNames:
                    linkTarget.addInput(arg)
                elif arg[:4] == "-Wl,":
                    linkerArgs = arg.split(",")
                    for linkerArg in linkerArgs:
                        if linkerArg in targetNames: linkTarget.addDependency(linkerArg)
                    ldflags.append(arg)
                elif arg[:2] == "-l":
                    candidate = "{}/lib{}.dylib".format(testDstDir, arg[2:])
                    if candidate in targetNames: linkTarget.addDependency(candidate)
                    ldflags.append(arg)
                elif arg[:7] == "-weak-l":
                    candidate = "{}/lib{}.dylib".format(testDstDir, arg[7:])
                    if candidate in targetNames: linkTarget.addDependency(candidate)
                    ldflags.append(arg)
                elif arg[:9] == "-upward-l":
                    candidate = "{}/lib{}.dylib".format(testDstDir, arg[9:])
                    if candidate in targetNames: linkTarget.addDependency(candidate)
                    ldflags.append(arg)
                else:
                    cflags.append(arg)
            if isMainExecutable:
                ldflags.append("-force_load $BUILT_PRODUCTS_DIR/libtest_support.a")
            for source in sources:
                objectHash = hashlib.sha1(linkTarget.output+source+tool+"".join(cflags)).hexdigest()
                target = ninja.newTarget(tool, "$OBJROOT/dyld_tests.build/Objects-normal/" + objectHash + ".o")
                target.addInput(source)
                target.dependencies = dependencies
                if cflags: target.addVariable("cflags", " ".join(cflags))
                if minOS: target.addVariable("minOS", minOS)
                linkTarget.addInput(target)
            if ldflags: linkTarget.addVariable("ldflags", " ".join(ldflags))
            if minOS: linkTarget.addVariable("minOS", minOS)
            installTarget = ninja.newTarget("install", "$INSTALL_DIR/AppleInternal/CoreOS/tests/dyld/{}".format(linkTarget.output[9:]))
            installTarget.addInput(linkTarget)
            testTarget.addInput(linkTarget)
            testInstallTarget.addInput(installTarget)
        else: raise ValueError("Unknown Command: {}".format(args[0]))
        
def processRunLines(ninja, runLines, testName, macOSBuild, symRoot, xcTestInvocations):
    runFilePath = "{}/{}/run.sh".format(symRoot, testName)
    for runLine in runLines:
        xcTestInvocations.append("{{ \"{}\", \"{}\" }}".format(testName, runLine.replace("\"","\\\"").replace("sudo","")))
    with BufferedFile(runFilePath) as runFile:
        runFile.write("#!/bin/sh\n")
        runFile.write("cd  {}\n".format(testRunDir))
        
        runFile.write("echo \"run in dyld2 mode\" \n");
        for runLine in runLines:
            runFile.write("TEST_OUTPUT=BATS TEST_DYLD_MODE=2 DYLD_USE_CLOSURES=0 {}\n".format(runLine))

        if macOSBuild:
            runFile.write("echo \"run in dyld2 mode with no shared cache\" \n");
            for runLine in runLines:
                runFile.write("TEST_OUTPUT=BATS TEST_DYLD_MODE=2 DYLD_SHARED_REGION=avoid {}\n".format(runLine))

        runFile.write("echo \"run in dyld3 mode\" \n");
        for runLine in runLines:
            if runLine.startswith("sudo "):
                runFile.write("sudo TEST_OUTPUT=BATS TEST_DYLD_MODE=3 DYLD_USE_CLOSURES=1 {}\n".format(runline[5:]))
            else:
                runFile.write("TEST_OUTPUT=BATS TEST_DYLD_MODE=3 DYLD_USE_CLOSURES=1 {}\n".format(runLine))

        if macOSBuild:
            runFile.write("echo \"run in dyld3 mode with no shared cache\" \n");
            for runLine in runLines:
                if runLine.startswith("sudo "):
                    runFile.write("sudo TEST_OUTPUT=BATS TEST_DYLD_MODE=3 DYLD_SHARED_REGION=avoid DYLD_USE_CLOSURES=1 {}\n".format(runline[5:]))
                else:
                    runFile.write("TEST_OUTPUT=BATS TEST_DYLD_MODE=3 DYLD_SHARED_REGION=avoid DYLD_USE_CLOSURES=1 {}\n".format(runLine))
    os.chmod(runFilePath, 0755)
    installPath = "$INSTALL_DIR/AppleInternal/CoreOS/tests/dyld/{}/run.sh".format(testName)
    target = ninja.newTarget("install", installPath)
    target.addInput(runFilePath)
    ninja.findTarget("install-{}".format(testName)).addInput(installPath)


if __name__ == "__main__":
    configPath = sys.argv[1]
    configMap = {}
    with open(configPath) as configFile:
        for line in configFile.read().splitlines():
            args = line.split()
            configMap[args[0]] = args[2:]
    sys.stderr.write("CONFIG: {}\n".format(configMap))
    srcRoot = configMap["SRCROOT"][0]
    symRoot = configMap["SYMROOT"][0]
    sdkRoot = configMap["SDKROOT"][0]
    objRoot = configMap["OBJROOT"][0]
    osFlag = configMap["OSFLAG"][0]
    osVers = configMap["OSVERSION"][0]
    linkerFlags = configMap["LDFLAGS"][0];
    installOwner = configMap["INSTALL_OWNER"][0];
    installGroup = configMap["INSTALL_GROUP"][0];
    installMode = configMap["INSTALL_MODE_FLAG"][0];
    installDir = configMap["INSTALL_DIR"][0];
    userHeaderSearchPaths = configMap["USER_HEADER_SEARCH_PATHS"]
    systemHeaderSearchPaths = configMap["SYSTEM_HEADER_SEARCH_PATHS"]

    derivedFilesDir = configMap["DERIVED_FILES_DIR"][0]
    archs = configMap["ARCHS"]

    if not os.path.exists(derivedFilesDir): os.makedirs(derivedFilesDir)
    if not os.path.exists(objRoot): os.makedirs(objRoot)

    sys.stderr.write("srcRoot = {}\n".format(srcRoot))
    sys.stderr.write("sdkRoot = {}\n".format(sdkRoot))
    sys.stderr.write("objRoot = {}\n".format(objRoot))
    sys.stderr.write("osFlag = {}\n".format(osFlag))
    sys.stderr.write("osVers = {}\n".format(osVers))
    sys.stderr.write("archs = {}\n".format(archs))
    sys.stderr.write("derivedFilesDir = {}\n".format(derivedFilesDir))

    testSrcRoot = os.path.abspath(srcRoot + "/testing/test-cases")
    ccTool = os.popen("xcrun --sdk " + sdkRoot + " --find clang").read().rstrip()
    cxxTool = os.popen("xcrun --sdk " + sdkRoot + " --find clang++").read().rstrip()
    headerPaths = " -isysroot " + sdkRoot
    for headerPath in userHeaderSearchPaths:  headerPaths += " -I{}".format(headerPath)
    for headerPath in systemHeaderSearchPaths:  headerPaths += " -I{}".format(headerPath)
    macOSBuild = False
    sudoCmd = ""
    if osFlag == "mmacosx-version-min":
        macOSBuild = True
        sudoCmd = "sudo"

    with NinjaFile(derivedFilesDir + "/build.ninja") as ninja:
        ninja.addInclude("config.ninja")
        ninja.addVariable("minOS", "-" + osFlag + "=" + osVers)
        ninja.addVariable("archs", " ".join(["-arch {}".format(arch) for arch in archs]))
        ninja.addVariable("mode", "0755")
        ninja.addVariable("headerpaths", headerPaths)

        ninja.addRule("cc", "{} -g -MMD -MF $out.d $archs -o $out -c $in $minOS $headerpaths $cflags".format(ccTool), "$out.d")
        ninja.addRule("cxx", "{} -g -MMD -MF $out.d $archs -o $out -c $in $minOS $headerpaths $cflags".format(cxxTool), "$out.d")
        ninja.addRule("cc-link", "{}  -g $archs -o $out -ltest_support $in $minOS -isysroot {} {} $ldflags && dsymutil -o $out.dSYM $out $extraCmds && codesign --force --sign - $entitlements $out".format(ccTool, sdkRoot, linkerFlags), False)
        ninja.addRule("cxx-link", "{}  -g $archs -o $out -ltest_support $in $minOS -isysroot {} {} $ldflags && dsymutil -o $out.dSYM $out $extraCmds && codesign --force --sign - $entitlements $out".format(cxxTool, sdkRoot, linkerFlags), False)
        ninja.addRule("dtrace", "/usr/sbin/dtrace -h -s $in -o $out", False)
        ninja.addRule("cp", "/bin/cp -p $in $out", False)
        ninja.addRule("install", "/usr/bin/install -m $mode -o {} -g {} $install_flags $in $out".format(installOwner, installGroup), False)
        ninja.addRule("symlink", "ln -sfh $source $out", False)

        allTarget = ninja.newTarget("phony", "all")
        masterInstallTarget = ninja.newTarget("phony", "install")

        runAllScriptPath = "{}/run_all_dyld_tests.sh".format(derivedFilesDir)
        xctestPath = "{}/dyld_xctest.h".format(derivedFilesDir)
        if "XCTestGenPath" in os.environ: xctestPath = os.environ["XCTestGenPath"]
        batsTests = []
        batsSuppressedCrashes = []
        xctestInvocations = []
        with BufferedFile(runAllScriptPath) as runAllScript:
            runAllScript.write("#!/bin/sh\n")
            for entry in os.listdir(testSrcRoot):
                if entry.endswith((".dtest")):
                    testName = entry[:-6]
                    sys.stdout.write("Processing " + testName + "\n")
                    runLines = []
                    buildLines = []
                    minOS = None

                    for file in os.listdir(testSrcRoot + "/" + entry):
                        testSrcDir = "$SRCROOT/testing/test-cases/{}.dtest".format(testName)
                        testDstDir = "$SYMROOT/{}".format(testName)
                        testRunDir = "/AppleInternal/CoreOS/tests/dyld/{}".format(testName)
                        buildSubs = {
                            "BUILD_DIR":            testDstDir,
                            "RUN_DIR":              testRunDir,
                            "SRC_DIR":              testSrcDir
                        }
                        runSubs = {
                            "RUN_DIR":        testRunDir,
                            "SUDO":           sudoCmd,
                        }
                        batsTest = {}
                        batsTest["TestName"] = testName
                        batsTest["Arch"] = "platform-native"
                        batsTest["WorkingDirectory"] = testRunDir
                        batsTest["ShowSubtestResults"] = True
                        batsTest["Command"] = []
                        batsTest["Command"].append("./run.sh")
                        if file.endswith((".c", ".cpp", ".cxx", ".m", ".mm")):
                            with open(testSrcRoot + "/" + entry + "/" + file) as f:
                                for line in f.read().splitlines():
                                    idx = string.find(line,"BUILD_ONLY:")
                                    if idx != -1:
                                        skippedOS = line[idx+11:].lstrip()
                                        if skippedOS == "MacOSX" and not macOSBuild: break
                                        else: continue
                                    idx = string.find(line,"BUILD_MIN_OS:")
                                    if idx != -1:
                                        minOS = "-" + osFlag + "=" + line[idx+13:].lstrip()
                                    idx = string.find(line,"BUILD:")
                                    if idx != -1:
                                        buildLines.append(string.Template(line[idx+6:]).safe_substitute(buildSubs))
                                        continue
                                    idx = string.find(line,"RUN:")
                                    if idx != -1:
                                        if "$SUDO" in line: batsTest["AsRoot"] = True
                                        runLines.append(string.Template(line[idx+4:]).safe_substitute(runSubs))
                                        continue
                                    idx = string.find(line,"RUN_TIMEOUT:")
                                    if idx != -1:
                                        batsTest["Timeout"] = line[idx+12:].lstrip()
                                        continue
                                    idx = string.find(line,"BOOT_ARGS:")
                                    if idx != -1:
                                        batsTest["BootArgsSet"] = ",".join(line[idx+9:].split())
                                        continue
                                    idx = string.find(line,"NO_CRASH_LOG:")
                                    if idx != -1:
                                        batsSuppressedCrashes.append(line[idx+13:].lstrip())
                                        continue
                    if buildLines and runLines:
                        processBuildLines(ninja, buildLines, testName, macOSBuild, minOS, testDstDir, testSrcDir)
                        processRunLines(ninja, runLines, testName, macOSBuild, symRoot, xctestInvocations)
                        runAllScript.write("/AppleInternal/CoreOS/tests/dyld/{}/run.sh\n".format(testName))
                        batsTests.append(batsTest)
            sys.stderr.write("Wrote test config to: {}".format(xctestPath))
            with BufferedFile(xctestPath) as xcTestFile:
                xcTestFile.write("static const TestInfo sTests[] = {\n")
                xcTestFile.write(",\n".join(xctestInvocations))
                xcTestFile.write("\n};")
        os.chmod(runAllScriptPath, 0755)
        runAllFilesInstallTarget = ninja.newTarget("install", "$INSTALL_DIR/AppleInternal/CoreOS/tests/dyld/run_all_dyld_tests.sh")
        runAllFilesInstallTarget.addInput("$DERIVED_FILES_DIR/run_all_dyld_tests.sh")
        masterInstallTarget.addInput(runAllFilesInstallTarget)
        batsFilePath = derivedFilesDir + "/dyld.plist"
        batsTests.sort(key=lambda test: test["TestName"])
        with BufferedFile(batsFilePath) as batsFile:
            batsConfig = { "BATSConfigVersion": "0.1.0",
                         "Project":           "dyld_tests",
                         "Tests":             batsTests }
            if batsSuppressedCrashes: batsConfig["IgnoreCrashes"] = batsSuppressedCrashes
            batsFile.write(plistlib.writePlistToString(batsConfig))
        os.system('plutil -convert binary1 ' + batsFilePath) # convert the plist in place to binary
        batsConfigInstallTarget = ninja.newTarget("install", "$INSTALL_DIR/AppleInternal/CoreOS/BATS/unit_tests/dyld.plist")
        batsConfigInstallTarget.addInput(batsFilePath)
        batsConfigInstallTarget.addVariable("mode", "0644")
        masterInstallTarget.addInput(batsConfigInstallTarget)
    sys.stdout.write("DONE\n")