This is xnu-12377.1.9. See this file in:
#!/usr/bin/env python3
import json
import argparse
import os
import pathlib
import xml.etree.ElementTree as ET
import uuid

# This scripts takes a compile_commands.json file that was generated using `make -C tests/unit cmds_json`
# and creates project files for an IDE that can be used for debugging user-space unit-tests
# The project is not able to build XNU or the test executable

SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))

TESTS_UNIT_PREFIX = "tests/unit/"
TESTS_UNIT_BUILD_PREFIX = TESTS_UNIT_PREFIX + "build/sym/"

def parse_command(entry):
    file = entry['file']
    directory = entry["directory"]
    if not file.startswith(SRC_ROOT):
        full_file = directory + "/" + file
    else:
        full_file = file
    assert full_file.startswith(SRC_ROOT), "unexpected path" + full_file
    rel_file = full_file[len(SRC_ROOT)+1:]

    # arguments[0] is clang
    args = entry['arguments'][1:]

    args.extend(['-I', directory])
    return rel_file, args

# -------------------------------------- Xcode project ----------------------------------------
# an Xcode project is a plist with a list of objects. each object has an ID and objects reference
# each other by their ID.

def do_quote_lst(dash_split):
    output = []
    # change ' -DX=y z' to ' -DX="y z"'
    for i, s in enumerate(dash_split):
        if i == 0:
            continue # skip the clang executable
        if '=' in s:
            st = s.strip()
            eq_sp = st.split('=')
            if ' ' in eq_sp[1]:
                output.append(f'{eq_sp[0]}=\\"{eq_sp[1]}\\"')
                continue

        output.append(f"{s}")
    return " ".join(output)

class ObjType:
    def __init__(self, idprefix, type_name):
        self.type_name = type_name
        self.id_prefix = idprefix
        self.next_count = 1
    def make_id(self):
        id = f"{self.id_prefix:016d}{self.next_count:08d}"
        self.next_count += 1
        return id

class ObjRegistry:
    def __init__(self):
        self.types = {}  # map type-name to id-prefix (12 chars)
        self.next_type_prefix = 1

        self.objects = {} # map object-id to instance

    def register(self, type_name, obj):
        if type_name not in self.types:
            self.types[type_name] = ObjType(self.next_type_prefix, type_name)
            self.next_type_prefix += 1
        id = self.types[type_name].make_id()
        self.objects[id] = obj
        return id


obj_reg = ObjRegistry()

TYPE_SOURCE_C = "sourcecode.c.c"
TYPE_SOURCE_CPP = "sourcecode.cpp.cpp"
TYPE_SOURCE_ASM = "sourcecode.asm"
TYPE_HEADER = "sourcecode.c.h"
TYPE_STATIC_LIB = "archive.ar"
TYPE_EXE = '"compiled.mach-o.executable"'

class ObjList:
    def __init__(self, name=None):
        self.name = name
        self.objs = []
    def add(self, obj):
        self.objs.append(obj)
    def extend(self, lst):
        self.objs.extend(lst)

def tab(count):
    return '\t' * count

# The top-level object list is special in that it's grouped by the type of objects
# This class represents part of the top level objects list
class TopObjList(ObjList):
    def write(self, out, lvl):
        out.write(f"/* Begin {self.name} section */\n")
        for obj in self.objs:
            out.write(f"{tab(lvl)}{obj.id} = ")
            obj.write(out, lvl)
        out.write(f"/* End {self.name} section */\n\n")

# a property that is serilized as a list of ids
class IdList(ObjList):
    def write(self, out, lvl):
        out.write("(\n") # after =
        for obj in self.objs:
            out.write(f"{tab(lvl+1)}{obj.id} /* {obj.name} */,\n")
        out.write(f"{tab(lvl)});\n")

class StrList:
    def __init__(self, lst):
        self.lst = lst
    def write(self, out, lvl):
        out.write("(\n") # after =
        for v in self.lst:
            out.write(f"{tab(lvl+1)}{v},\n")
        out.write(f"{tab(lvl)});\n")
    @classmethod
    def list_sort_quote(cls, s):
        l = list(s)
        l.sort()
        return cls([f'"{d}"' for d in l])

class StrEval:
    def __init__(self, fn):
        self.fn = fn
    def write(self, out, lvl):
        out.write(self.fn() + ";\n")
class LateEval:
    def __init__(self, fn):
        self.fn = fn
    def write(self, out, lvl):
        self.fn().write(out, lvl)

class PDict:
    def __init__(self, isa, inline=False):
        self.d = {}
        self.p = []
        self.inline = inline
        if isa is not None:
            self.isa = self.padd("isa", isa)

    def padd(self, k, v, comment=None):
        self.p.append((k, v, comment))
        self.d[k] = v
        return v
    def pextend(self, d):
        for k, v in d.items():
            self.padd(k, v)

    def write(self, out, lvl):
        if self.inline:
            out.write("{")
            for k, v, comment in self.p:
                assert isinstance(v, str) or isinstance(v, int), "complex value inline"
                out.write(f"{k} = ")
                if comment is None:
                    out.write(f"{v}; ")
                else:
                    out.write(f"{v} /* {comment} */; ")
            out.write("};\n")
        else:
            out.write("{\n")  # comes after =
            for k, v, comment in self.p:
                out.write(f"{tab(lvl+1)}{k} = ")
                if isinstance(v, str) or isinstance(v, int):
                    if comment is None:
                        out.write(f"{v};\n")
                    else:
                        out.write(f"{v} /* {comment} */;\n")
                else:
                    v.write(out, lvl+1)
            out.write(f"{tab(lvl)}}};\n")


class File:
    def __init__(self, name, args):
        self.name = name.split('/')[-1]
        self.args = args
        self.ref = None

    def type_str(self):
        ext = os.path.splitext(self.name)[1]
        if ext == ".c":
            return TYPE_SOURCE_C
        if ext == ".h":
            return TYPE_HEADER
        if ext == ".cpp":
            return TYPE_SOURCE_CPP
        if ext == ".a":
            return TYPE_STATIC_LIB
        if ext == ".s":
            return TYPE_SOURCE_ASM
        if ext == '':
            return TYPE_EXE
        return None

class BuildFile(PDict):
    def __init__(self, file):
        PDict.__init__(self, "PBXBuildFile", inline=True)
        self.id = obj_reg.register("build_file", self)
        self.file = file
        self.name = file.name
        self.padd("fileRef", self.file.ref.id, comment=self.file.name)

class FileRef(PDict):
    def __init__(self, file):
        PDict.__init__(self, "PBXFileReference", inline=True)
        self.id = obj_reg.register("file_ref", self)
        self.file = file
        file.ref = self
        typ = self.file.type_str()
        assert typ is not None, "unknown file type " + self.file.name
        if typ == TYPE_STATIC_LIB or typ == TYPE_EXE:
            self.padd("explicitFileType", typ)
            self.padd("includeInIndex", 0)
            self.padd("path", f'"{self.file.name}"')
            self.padd("sourceTree", "BUILT_PRODUCTS_DIR")
        else:
            self.padd("lastKnownFileType", typ)
            self.padd("path", f'"{self.file.name}"')
            self.padd("sourceTree", '"<group>"')

    @property
    def name(self):
        return self.file.name

class Group(PDict):
    def __init__(self, name=None, path=None):
        PDict.__init__(self, "PBXGroup")
        self.id = obj_reg.register("group", self)
        self.children = self.padd("children", IdList())
        self.child_dict = {}  # map name to Group/FileRef
        if name is not None:
            self.name = self.padd("name", name)
        if path is not None:
            self.name = self.padd("path", f'"{path}"')
        self.padd("sourceTree", '"<group>"')

    def rec_add(self, sp_path, groups_lst, file_ref):
        elem = sp_path[0]
        if len(sp_path) == 1:
            assert elem not in self.child_dict, f"already have file elem {elem} in {self.name}"
            self.children.add(file_ref)
            self.child_dict[elem] = file_ref
            #file_ref.file.name = elem # remove the path from the name
        else:
            if elem in self.child_dict:
                g = self.child_dict[elem]
            else:
                g = Group(path=elem)
                groups_lst.add(g)
                self.children.add(g)
                self.child_dict[elem] = g
            g.rec_add(sp_path[1:], groups_lst, file_ref)

    def sort(self):
        self.children.objs.sort(key=lambda x: x.name)
        for elem in self.children.objs:
            if isinstance(elem, Group):
                elem.sort()

class BuildPhase(PDict):
    def __init__(self, isa, name):
        PDict.__init__(self, isa)
        self.id = obj_reg.register("build_phase", self)
        self.name = name
        self.padd("buildActionMask", 2147483647)
        self.files = self.padd("files", IdList())
        self.padd("runOnlyForDeploymentPostprocessing", 0)

class Target(PDict):
    def __init__(self, name, file_ref, cfg_lst, prod_type):
        PDict.__init__(self, "PBXNativeTarget")
        self.id = obj_reg.register("target", self)
        self.cfg_lst = self.padd("buildConfigurationList", cfg_lst.id)
        self.build_phases = self.padd("buildPhases", IdList())
        self.padd("buildRules", IdList())
        self.padd("dependencies", IdList())
        self.name = self.padd("name", name)
        self.padd("packageProductDependencies", IdList())
        self.padd("productName", name)
        self.padd("productReference", file_ref.id, comment=file_ref.name)
        self.padd("productType", prod_type)

class CfgList(PDict):
    def __init__(self, name):
        PDict.__init__(self, "XCConfigurationList")
        self.id = obj_reg.register("config_list", self)
        self.name = name # not used
        self.configs = self.padd("buildConfigurations", IdList())
        self.padd("defaultConfigurationIsVisible", 0)
        self.padd("defaultConfigurationName", StrEval(lambda: self.configs.objs[0].name))

class Config(PDict):
    def __init__(self, name):
        PDict.__init__(self, "XCBuildConfiguration")
        self.id = obj_reg.register("config", self)
        self.settings = self.padd("buildSettings", PDict(None))
        self.name = self.padd("name", name)

class Project(PDict):
    def __init__(self, cfg_lst, group_main, group_prod):
        PDict.__init__(self, "PBXProject")
        self.id = obj_reg.register("project", self)
        self.targets = IdList("targets")
        self.padd("attributes", LateEval(lambda: self.make_attr()))
        self.padd("buildConfigurationList", cfg_lst.id, comment=cfg_lst.name)
        self.padd("developmentRegion", "en")
        self.padd("hasScannedForEncodings", "0")
        self.padd("knownRegions", StrList(["en", "Base"]))
        self.padd("mainGroup", group_main.id)
        self.padd("minimizedProjectReferenceProxies", "1")
        self.padd("preferredProjectObjectVersion", "77")
        self.padd("productRefGroup", group_prod.id)
        self.padd("projectDirPath", '""')
        self.padd("projectRoot", '""')
        self.padd("targets", self.targets)

    def make_attr(self):
        a = PDict(None)
        a.padd("BuildIndependentTargetsInParallel", 1)
        a.padd("LastUpgradeCheck", 1700)
        ta = a.padd("TargetAttributes", PDict(None))
        for t in self.targets.objs:
            p = ta.padd(t.id, PDict(None))
            p.padd("CreatedOnToolsVersion", "17.0")
        return a


class PbxProj:
    def __init__(self):
        self.top_obj = []
        self.build_files = self.add_top(TopObjList("PBXBuildFile"))
        self.file_refs = self.add_top(TopObjList("PBXFileReference"))
        self.groups = self.add_top(TopObjList("PBXGroup"))
        self.build_phases = self.add_top(TopObjList("build phases"))
        self.targets = self.add_top(TopObjList("PBXNativeTarget"))
        self.projects = self.add_top(TopObjList("PBXProject"))
        self.configs = self.add_top(TopObjList("XCBuildConfiguration"))
        self.config_lists = self.add_top(TopObjList("XCConfigurationList"))

        self.group_main = self.add_group(Group())
        self.group_products = self.add_group(Group(name="Products"))
        self.group_main.children.add(self.group_products)

        self.cfg_prod_release = self.add_config(Config("Release"))
        self.cfg_prod_release.settings.pextend({"SDKROOT": "macosx",
                                           "MACOSX_DEPLOYMENT_TARGET": "14.1",
                                           })
        self.proj_cfg_lst = self.add_cfg_lst(CfgList("proj config list"))
        self.proj_cfg_lst.configs.add(self.cfg_prod_release)

        self.root_proj = Project(self.proj_cfg_lst, self.group_main, self.group_products)
        self.projects.add(self.root_proj)

        self.test_exec = []

    def add_top(self, t):
        self.top_obj.append(t)
        return t
    def add_group(self, g):
        self.groups.add(g)
        return g
    def add_build_phase(self, p):
        self.build_phases.add(p)
        return p
    def add_config(self, c):
        self.configs.add(c)
        return c
    def add_cfg_lst(self, c):
        self.config_lists.add(c)
        return c
    def add_target(self, t):
        self.targets.add(t)
        return t

    def add_xnu_archive(self):
        f = File("libkernel.a", [])
        fr = FileRef(f)
        self.file_refs.add(fr)
        self.group_products.children.add(fr)
        self.xnu_phase_headers = self.add_build_phase(BuildPhase("PBXHeadersBuildPhase", "Headers"))
        self.xnu_phase_sources = self.add_build_phase(BuildPhase("PBXSourcesBuildPhase", "Sources"))

        cfg_xnu_release = self.add_config(Config("Release"))
        cfg_xnu_release.settings.pextend( { "CODE_SIGN_STYLE": "Automatic",
                                            "EXECUTABLE_PREFIX": "lib",
                                            "PRODUCT_NAME": '"$(TARGET_NAME)"',
                                            "SKIP_INSTALL": "YES"})
        xnu_cfg_lst = self.add_cfg_lst(CfgList("target config list"))
        xnu_cfg_lst.configs.add(cfg_xnu_release)

        target = self.add_target(Target("xnu_static_lib", fr, xnu_cfg_lst, '"com.apple.product-type.library.static"'))
        target.build_phases.extend([self.xnu_phase_headers, self.xnu_phase_sources])
        self.root_proj.targets.add(target)

    def add_test_target(self, c_file_ref, c_build_file):
        name = os.path.splitext(os.path.split(c_file_ref.name)[1])[0]
        f = File(name, [])
        fr = FileRef(f)
        self.file_refs.add(fr)
        self.group_products.children.add(fr)
        phase_h = self.add_build_phase(BuildPhase("PBXHeadersBuildPhase", "Headers"))
        phase_src = self.add_build_phase(BuildPhase("PBXSourcesBuildPhase", "Sources"))
        phase_src.files.add(c_build_file)

        cfg_release = self.add_config(Config("Release"))
        cfg_release.settings.pextend( { "CODE_SIGN_STYLE": "Automatic",
                                        "PRODUCT_NAME": '"$(TARGET_NAME)"'})
        cfg_lst = self.add_cfg_lst(CfgList("target config list"))
        cfg_lst.configs.add(cfg_release)

        target = self.add_target(Target(name, fr, cfg_lst, '"com.apple.product-type.tool"'))
        target.build_phases.extend([phase_h, phase_src])
        self.root_proj.targets.add(target)
        self.test_exec.append(target)

    def add_file(self, file_path, flags):
        f = File(file_path, flags)
        fr = FileRef(f)
        bf = BuildFile(f)
        self.build_files.add(bf)
        self.file_refs.add(fr)
        self.group_main.rec_add(file_path.split('/'), self.groups, fr)
        typ = f.type_str()
        if typ == TYPE_HEADER:
            self.xnu_phase_headers.files.add(bf)
        elif typ in [TYPE_SOURCE_C, TYPE_SOURCE_CPP, TYPE_SOURCE_ASM]:
            self.xnu_phase_sources.files.add(bf)
        return fr, bf
    def add_ccj(self, ccj):
        test_targets = []
        for entry in ccj:
            src_file, flags = parse_command(entry)
            if src_file.endswith('dt_proxy.c'):
                continue
            fr, bf = self.add_file(src_file, flags)
            if src_file.startswith(TESTS_UNIT_PREFIX):
                test_targets.append((fr, bf))
        test_targets.sort(key=lambda x:x[1].name)
        for fr, bf in test_targets:
            self.add_test_target(fr, bf)

    def add_headers(self):
        for path in pathlib.Path(SRC_ROOT).rglob('*.h'):
            full_file = str(path)
            assert full_file.startswith(SRC_ROOT), "unexpected path" + full_file
            rel_file = full_file[len(SRC_ROOT)+1:]
            self.add_file(str(rel_file), None)

    def sort_groups(self):
        self.group_main.sort()

    def write(self, out):
        out.write("// !$*UTF8*$!\n{\n")
        out.write("\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 77;\n\tobjects = {\n\n")
        for t in self.top_obj:
            t.write(out, 2)
        out.write(f"\t}};\n\trootObject = {self.root_proj.id};\n")
        out.write("}")

    def make_settings(self):
        # go over all build files and find in their arguments a union of all the included folders
        # this is useful for file navigation in xcode to work correctly
        inc_dirs = set()
        common_defines = None
        for f in self.build_files.objs:
            file_defines = set()
            args = f.file.args
            if args is None:
                continue
            for i, arg in enumerate(args):
                if arg == '-I':
                    d = args[i + 1]
                    if d != ".":
                        inc_dirs.add(args[i + 1])
                elif arg == '-D':
                    file_defines.add(args[i+1])
            if common_defines is None:
                common_defines = file_defines
            else:
                common_defines = common_defines.intersection(file_defines)
        inc_str_lst = StrList.list_sort_quote(inc_dirs)
        self.cfg_prod_release.settings.padd("HEADER_SEARCH_PATHS", inc_str_lst)
        self.cfg_prod_release.settings.padd("SYSTEM_HEADER_SEARCH_PATHS", inc_str_lst)
        str_common_defs = StrList.list_sort_quote(common_defines)
        self.cfg_prod_release.settings.padd("GCC_PREPROCESSOR_DEFINITIONS", str_common_defs)

    def write_schemes(self, folder, container_dir):
        for target in self.test_exec:
            path = os.path.join(folder, target.name + ".xcscheme")
            out = open(path, "w")
            exec_path = SRC_ROOT + "/" + TESTS_UNIT_BUILD_PREFIX + target.name
            out.write(f'''<?xml version="1.0" encoding="UTF-8"?>
<Scheme
   LastUpgradeVersion = "1630"
   version = "1.7">
   <BuildAction
      parallelizeBuildables = "YES"
      buildImplicitDependencies = "YES"
      buildArchitectures = "Automatic">
      <BuildActionEntries>
         <BuildActionEntry
            buildForTesting = "NO"
            buildForRunning = "NO"
            buildForProfiling = "YES"
            buildForArchiving = "NO"
            buildForAnalyzing = "NO">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "{target.id}"
               BuildableName = "{target.name}"
               BlueprintName = "{target.name}"
               ReferencedContainer = "container:{container_dir}">
            </BuildableReference>
         </BuildActionEntry>
      </BuildActionEntries>
   </BuildAction>
   <LaunchAction
      buildConfiguration = "Release"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      launchStyle = "0"
      useCustomWorkingDirectory = "NO"
      ignoresPersistentStateOnLaunch = "NO"
      debugDocumentVersioning = "YES"
      debugServiceExtension = "internal"
      allowLocationSimulation = "YES"
      internalIOSLaunchStyle = "3"
      viewDebuggingEnabled = "No">
      <PathRunnable
         runnableDebuggingMode = "0"
         FilePath = "{exec_path}">
      </PathRunnable>
      <MacroExpansion>
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "{target.id}"
            BuildableName = "{target.name}"
            BlueprintName = "{target.name}"
            ReferencedContainer = "container:{container_dir}">
         </BuildableReference>
      </MacroExpansion>
   </LaunchAction>
</Scheme>   
''')
            print(f"Wrote {path}")

def gen_xcode(ccj):
    p = PbxProj()
    p.add_xnu_archive()
    p.add_ccj(ccj)
    p.add_headers()
    p.sort_groups()
    p.make_settings()

    output = os.path.join(SRC_ROOT, "ut_xnu_proj.xcodeproj")
    os.makedirs(output, exist_ok=True)
    proj_path = os.path.join(output, "project.pbxproj")
    p.write(open(proj_path, "w"))
    print(f'wrote file: {proj_path};')

    schemes_dir = output + "/xcshareddata/xcschemes"
    os.makedirs(schemes_dir, exist_ok=True)
    p.write_schemes(schemes_dir, output)
    print(f'wrote schemes to: {schemes_dir}')

# -------------------------------------- VSCode launch targets ----------------------------------------

class TargetsProject:
    def __init__(self):
        self.targets = []

    def add_ccj(self, ccj):
        for entry in ccj:
            src_file, flags = parse_command(entry)
            if src_file.startswith(TESTS_UNIT_PREFIX):
                name = os.path.splitext(src_file[len(TESTS_UNIT_PREFIX):])[0]
                self.targets.append(name)
        self.targets.sort()

class VsCodeLaunchJson(TargetsProject):
    def write(self, f):
        confs = []
        launch = {"version": "0.2.0", "configurations": confs }
        for t in self.targets:
            confs.append({
                "name": t,
                "type": "lldb-dap",
                "request": "launch",
                "program": "${workspaceFolder}/" + TESTS_UNIT_BUILD_PREFIX + t,
                "stopOnEntry": False,
                "cwd": "${workspaceFolder}",
                "args": [],
                "env": []
            })
        json.dump(launch, f, indent=4)


def gen_vscode(ccj):
    p = VsCodeLaunchJson()
    p.add_ccj(ccj)

    output = os.path.join(SRC_ROOT, ".vscode/launch.json")
    os.makedirs(os.path.join(SRC_ROOT, ".vscode"), exist_ok=True)
    if os.path.exists(output):
        print(f"deleting existing {output}")
        os.unlink(output)
    p.write(open(output, "w"))
    print(f"wrote {output}")

# -------------------------------------- CLion targets ----------------------------------------

def find_elem(root, tag, **kvarg):
    assert len(kvarg.items()) == 1
    key, val = list(kvarg.items())[0]
    for child in root:
        assert child.tag == tag, f'unexpected child.tag {child.tag}'
        if child.attrib[key] == val:
            return child
    return None

def get_elem(root, tag, **kvarg):
    child = find_elem(root, tag, **kvarg)
    key, val = list(kvarg.items())[0]
    if child is not None:
        return child, False
    comp = ET.SubElement(root, tag)
    comp.attrib[key] = val
    return comp, True


CLION_TOOLCHAIN_NAME = "System"
class CLionProject(TargetsProject):
    def _get_root(self, path):
        if os.path.exists(path):
            print(f"Parsing existing file {path}")
            root = ET.parse(path).getroot()
            assert root.tag == 'project', f'unexpected root.tag {root.tag}'
        else:
            root = ET.Element('project')
            root.attrib["version"] = "4"
        return root

    def _write(self, root, path):
        tree = ET.ElementTree(root)
        ET.indent(tree, space='  ', level=0)
        tree.write(open(path, "wb"), encoding="utf-8", xml_declaration=True)
        print(f"Wrote {path}")

    def make_custom_targets(self):
        # add a target that uses toolchain "System"
        path = os.path.join(SRC_ROOT, ".idea/customTargets.xml")
        root = self._get_root(path)
        comp, _ = get_elem(root, "component", name="CLionExternalBuildManager")
        # check if we already have the target we need
        for target in comp:
            if target.attrib["defaultType"] == "TOOL":
                target_name = target.attrib["name"]
                if len(target) == 1 and target[0].tag == "configuration":
                    conf = target[0]
                    if conf.attrib["toolchainName"] == CLION_TOOLCHAIN_NAME:
                        conf_name = conf.attrib["name"]
                        print(f"file {path} already has the needed target with name {target_name},{conf_name}")
                        return target_name, conf_name # it already exists, nothing to do
        # add a new target
        target_name = "test_default"
        conf_name = "test_default"

        target = ET.SubElement(comp, "target")
        target.attrib["id"] = str(uuid.uuid1())
        target.attrib["name"] = target_name
        target.attrib["defaultType"] = "TOOL"

        conf = ET.SubElement(target, "configuration")
        conf.attrib["id"] = str(uuid.uuid1())
        conf.attrib["name"] = conf_name
        conf.attrib["toolchainName"] = CLION_TOOLCHAIN_NAME
        print(f"Created target named {target_name}")
        self._write(root, path)
        return target_name, conf_name

    def add_to_workspace(self, target_name, conf_name):
        path = os.path.join(SRC_ROOT, ".idea/workspace.xml")
        root = self._get_root(path)
        comp, _ = get_elem(root, "component", name="RunManager")
        added_anything = False
        for t in self.targets:
            for conf in comp:
                if conf.tag != "configuration":
                    continue
                if conf.attrib["name"] == t:  # already has this target
                    print(f"Found existing configuration named '{t}', not adding it")
                    break
            else:
                print(f"Adding configuration for '{t}'")
                proj_name = os.path.basename(SRC_ROOT)
                conf = ET.SubElement(comp, "configuration", name=t,
                                     type="CLionExternalRunConfiguration",
                                     factoryName="Application",
                                     REDIRECT_INPUT="false",
                                     ELEVATE="false",
                                     USE_EXTERNAL_CONSOLE="false",
                                     EMULATE_TERMINAL="false",
                                     PASS_PARENT_ENVS_2="true",
                                     PROJECT_NAME=proj_name,
                                     TARGET_NAME=target_name,
                                     CONFIG_NAME=conf_name,
                                     RUN_PATH=f"$PROJECT_DIR$/{TESTS_UNIT_BUILD_PREFIX}{t}")
                ET.SubElement(conf, "method", v="2")
                added_anything = True
        if added_anything:
            self._write(root, path)


def gen_clion(ccj):
    p = CLionProject()
    p.add_ccj(ccj)

    os.makedirs(os.path.join(SRC_ROOT, ".idea"), exist_ok=True)
    target_name, conf_name = p.make_custom_targets()
    p.add_to_workspace(target_name, conf_name)


def main():
    parser = argparse.ArgumentParser(description='Generate xcode project from compile_commands.json')
    parser.add_argument('mode', help='IDE to generate for', choices=['xcode', 'vscode', 'clion'])
    parser.add_argument('compile_commands', help='Path to compile_commands.json', nargs='*', default=os.path.join(SRC_ROOT, "compile_commands.json"))
    args = parser.parse_args()

    if not os.path.exists(args.compile_commands):
        print(f"Can't find input {args.compile_commands}")
        return 1

    ccj = json.load(open(args.compile_commands, 'r'))

    if args.mode == 'xcode':
        return gen_xcode(ccj)
    elif args.mode == 'vscode':
        return gen_vscode(ccj)
    elif args.mode == 'clion':
        return gen_clion(ccj)


if __name__ == '__main__':
    main()