xref: /xnu-12377.1.9/tests/unit/tools/generate_ut_proj.py (revision f6217f891ac0bb64f3d375211650a4c1ff8ca1ea)
1#!/usr/bin/env python3
2import json
3import argparse
4import os
5import pathlib
6import xml.etree.ElementTree as ET
7import uuid
8
9# This scripts takes a compile_commands.json file that was generated using `make -C tests/unit cmds_json`
10# and creates project files for an IDE that can be used for debugging user-space unit-tests
11# The project is not able to build XNU or the test executable
12
13SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
14
15TESTS_UNIT_PREFIX = "tests/unit/"
16TESTS_UNIT_BUILD_PREFIX = TESTS_UNIT_PREFIX + "build/sym/"
17
18def parse_command(entry):
19    file = entry['file']
20    directory = entry["directory"]
21    if not file.startswith(SRC_ROOT):
22        full_file = directory + "/" + file
23    else:
24        full_file = file
25    assert full_file.startswith(SRC_ROOT), "unexpected path" + full_file
26    rel_file = full_file[len(SRC_ROOT)+1:]
27
28    # arguments[0] is clang
29    args = entry['arguments'][1:]
30
31    args.extend(['-I', directory])
32    return rel_file, args
33
34# -------------------------------------- Xcode project ----------------------------------------
35# an Xcode project is a plist with a list of objects. each object has an ID and objects reference
36# each other by their ID.
37
38def do_quote_lst(dash_split):
39    output = []
40    # change ' -DX=y z' to ' -DX="y z"'
41    for i, s in enumerate(dash_split):
42        if i == 0:
43            continue # skip the clang executable
44        if '=' in s:
45            st = s.strip()
46            eq_sp = st.split('=')
47            if ' ' in eq_sp[1]:
48                output.append(f'{eq_sp[0]}=\\"{eq_sp[1]}\\"')
49                continue
50
51        output.append(f"{s}")
52    return " ".join(output)
53
54class ObjType:
55    def __init__(self, idprefix, type_name):
56        self.type_name = type_name
57        self.id_prefix = idprefix
58        self.next_count = 1
59    def make_id(self):
60        id = f"{self.id_prefix:016d}{self.next_count:08d}"
61        self.next_count += 1
62        return id
63
64class ObjRegistry:
65    def __init__(self):
66        self.types = {}  # map type-name to id-prefix (12 chars)
67        self.next_type_prefix = 1
68
69        self.objects = {} # map object-id to instance
70
71    def register(self, type_name, obj):
72        if type_name not in self.types:
73            self.types[type_name] = ObjType(self.next_type_prefix, type_name)
74            self.next_type_prefix += 1
75        id = self.types[type_name].make_id()
76        self.objects[id] = obj
77        return id
78
79
80obj_reg = ObjRegistry()
81
82TYPE_SOURCE_C = "sourcecode.c.c"
83TYPE_SOURCE_CPP = "sourcecode.cpp.cpp"
84TYPE_SOURCE_ASM = "sourcecode.asm"
85TYPE_HEADER = "sourcecode.c.h"
86TYPE_STATIC_LIB = "archive.ar"
87TYPE_EXE = '"compiled.mach-o.executable"'
88
89class ObjList:
90    def __init__(self, name=None):
91        self.name = name
92        self.objs = []
93    def add(self, obj):
94        self.objs.append(obj)
95    def extend(self, lst):
96        self.objs.extend(lst)
97
98def tab(count):
99    return '\t' * count
100
101# The top-level object list is special in that it's grouped by the type of objects
102# This class represents part of the top level objects list
103class TopObjList(ObjList):
104    def write(self, out, lvl):
105        out.write(f"/* Begin {self.name} section */\n")
106        for obj in self.objs:
107            out.write(f"{tab(lvl)}{obj.id} = ")
108            obj.write(out, lvl)
109        out.write(f"/* End {self.name} section */\n\n")
110
111# a property that is serilized as a list of ids
112class IdList(ObjList):
113    def write(self, out, lvl):
114        out.write("(\n") # after =
115        for obj in self.objs:
116            out.write(f"{tab(lvl+1)}{obj.id} /* {obj.name} */,\n")
117        out.write(f"{tab(lvl)});\n")
118
119class StrList:
120    def __init__(self, lst):
121        self.lst = lst
122    def write(self, out, lvl):
123        out.write("(\n") # after =
124        for v in self.lst:
125            out.write(f"{tab(lvl+1)}{v},\n")
126        out.write(f"{tab(lvl)});\n")
127    @classmethod
128    def list_sort_quote(cls, s):
129        l = list(s)
130        l.sort()
131        return cls([f'"{d}"' for d in l])
132
133class StrEval:
134    def __init__(self, fn):
135        self.fn = fn
136    def write(self, out, lvl):
137        out.write(self.fn() + ";\n")
138class LateEval:
139    def __init__(self, fn):
140        self.fn = fn
141    def write(self, out, lvl):
142        self.fn().write(out, lvl)
143
144class PDict:
145    def __init__(self, isa, inline=False):
146        self.d = {}
147        self.p = []
148        self.inline = inline
149        if isa is not None:
150            self.isa = self.padd("isa", isa)
151
152    def padd(self, k, v, comment=None):
153        self.p.append((k, v, comment))
154        self.d[k] = v
155        return v
156    def pextend(self, d):
157        for k, v in d.items():
158            self.padd(k, v)
159
160    def write(self, out, lvl):
161        if self.inline:
162            out.write("{")
163            for k, v, comment in self.p:
164                assert isinstance(v, str) or isinstance(v, int), "complex value inline"
165                out.write(f"{k} = ")
166                if comment is None:
167                    out.write(f"{v}; ")
168                else:
169                    out.write(f"{v} /* {comment} */; ")
170            out.write("};\n")
171        else:
172            out.write("{\n")  # comes after =
173            for k, v, comment in self.p:
174                out.write(f"{tab(lvl+1)}{k} = ")
175                if isinstance(v, str) or isinstance(v, int):
176                    if comment is None:
177                        out.write(f"{v};\n")
178                    else:
179                        out.write(f"{v} /* {comment} */;\n")
180                else:
181                    v.write(out, lvl+1)
182            out.write(f"{tab(lvl)}}};\n")
183
184
185class File:
186    def __init__(self, name, args):
187        self.name = name.split('/')[-1]
188        self.args = args
189        self.ref = None
190
191    def type_str(self):
192        ext = os.path.splitext(self.name)[1]
193        if ext == ".c":
194            return TYPE_SOURCE_C
195        if ext == ".h":
196            return TYPE_HEADER
197        if ext == ".cpp":
198            return TYPE_SOURCE_CPP
199        if ext == ".a":
200            return TYPE_STATIC_LIB
201        if ext == ".s":
202            return TYPE_SOURCE_ASM
203        if ext == '':
204            return TYPE_EXE
205        return None
206
207class BuildFile(PDict):
208    def __init__(self, file):
209        PDict.__init__(self, "PBXBuildFile", inline=True)
210        self.id = obj_reg.register("build_file", self)
211        self.file = file
212        self.name = file.name
213        self.padd("fileRef", self.file.ref.id, comment=self.file.name)
214
215class FileRef(PDict):
216    def __init__(self, file):
217        PDict.__init__(self, "PBXFileReference", inline=True)
218        self.id = obj_reg.register("file_ref", self)
219        self.file = file
220        file.ref = self
221        typ = self.file.type_str()
222        assert typ is not None, "unknown file type " + self.file.name
223        if typ == TYPE_STATIC_LIB or typ == TYPE_EXE:
224            self.padd("explicitFileType", typ)
225            self.padd("includeInIndex", 0)
226            self.padd("path", f'"{self.file.name}"')
227            self.padd("sourceTree", "BUILT_PRODUCTS_DIR")
228        else:
229            self.padd("lastKnownFileType", typ)
230            self.padd("path", f'"{self.file.name}"')
231            self.padd("sourceTree", '"<group>"')
232
233    @property
234    def name(self):
235        return self.file.name
236
237class Group(PDict):
238    def __init__(self, name=None, path=None):
239        PDict.__init__(self, "PBXGroup")
240        self.id = obj_reg.register("group", self)
241        self.children = self.padd("children", IdList())
242        self.child_dict = {}  # map name to Group/FileRef
243        if name is not None:
244            self.name = self.padd("name", name)
245        if path is not None:
246            self.name = self.padd("path", f'"{path}"')
247        self.padd("sourceTree", '"<group>"')
248
249    def rec_add(self, sp_path, groups_lst, file_ref):
250        elem = sp_path[0]
251        if len(sp_path) == 1:
252            assert elem not in self.child_dict, f"already have file elem {elem} in {self.name}"
253            self.children.add(file_ref)
254            self.child_dict[elem] = file_ref
255            #file_ref.file.name = elem # remove the path from the name
256        else:
257            if elem in self.child_dict:
258                g = self.child_dict[elem]
259            else:
260                g = Group(path=elem)
261                groups_lst.add(g)
262                self.children.add(g)
263                self.child_dict[elem] = g
264            g.rec_add(sp_path[1:], groups_lst, file_ref)
265
266    def sort(self):
267        self.children.objs.sort(key=lambda x: x.name)
268        for elem in self.children.objs:
269            if isinstance(elem, Group):
270                elem.sort()
271
272class BuildPhase(PDict):
273    def __init__(self, isa, name):
274        PDict.__init__(self, isa)
275        self.id = obj_reg.register("build_phase", self)
276        self.name = name
277        self.padd("buildActionMask", 2147483647)
278        self.files = self.padd("files", IdList())
279        self.padd("runOnlyForDeploymentPostprocessing", 0)
280
281class Target(PDict):
282    def __init__(self, name, file_ref, cfg_lst, prod_type):
283        PDict.__init__(self, "PBXNativeTarget")
284        self.id = obj_reg.register("target", self)
285        self.cfg_lst = self.padd("buildConfigurationList", cfg_lst.id)
286        self.build_phases = self.padd("buildPhases", IdList())
287        self.padd("buildRules", IdList())
288        self.padd("dependencies", IdList())
289        self.name = self.padd("name", name)
290        self.padd("packageProductDependencies", IdList())
291        self.padd("productName", name)
292        self.padd("productReference", file_ref.id, comment=file_ref.name)
293        self.padd("productType", prod_type)
294
295class CfgList(PDict):
296    def __init__(self, name):
297        PDict.__init__(self, "XCConfigurationList")
298        self.id = obj_reg.register("config_list", self)
299        self.name = name # not used
300        self.configs = self.padd("buildConfigurations", IdList())
301        self.padd("defaultConfigurationIsVisible", 0)
302        self.padd("defaultConfigurationName", StrEval(lambda: self.configs.objs[0].name))
303
304class Config(PDict):
305    def __init__(self, name):
306        PDict.__init__(self, "XCBuildConfiguration")
307        self.id = obj_reg.register("config", self)
308        self.settings = self.padd("buildSettings", PDict(None))
309        self.name = self.padd("name", name)
310
311class Project(PDict):
312    def __init__(self, cfg_lst, group_main, group_prod):
313        PDict.__init__(self, "PBXProject")
314        self.id = obj_reg.register("project", self)
315        self.targets = IdList("targets")
316        self.padd("attributes", LateEval(lambda: self.make_attr()))
317        self.padd("buildConfigurationList", cfg_lst.id, comment=cfg_lst.name)
318        self.padd("developmentRegion", "en")
319        self.padd("hasScannedForEncodings", "0")
320        self.padd("knownRegions", StrList(["en", "Base"]))
321        self.padd("mainGroup", group_main.id)
322        self.padd("minimizedProjectReferenceProxies", "1")
323        self.padd("preferredProjectObjectVersion", "77")
324        self.padd("productRefGroup", group_prod.id)
325        self.padd("projectDirPath", '""')
326        self.padd("projectRoot", '""')
327        self.padd("targets", self.targets)
328
329    def make_attr(self):
330        a = PDict(None)
331        a.padd("BuildIndependentTargetsInParallel", 1)
332        a.padd("LastUpgradeCheck", 1700)
333        ta = a.padd("TargetAttributes", PDict(None))
334        for t in self.targets.objs:
335            p = ta.padd(t.id, PDict(None))
336            p.padd("CreatedOnToolsVersion", "17.0")
337        return a
338
339
340class PbxProj:
341    def __init__(self):
342        self.top_obj = []
343        self.build_files = self.add_top(TopObjList("PBXBuildFile"))
344        self.file_refs = self.add_top(TopObjList("PBXFileReference"))
345        self.groups = self.add_top(TopObjList("PBXGroup"))
346        self.build_phases = self.add_top(TopObjList("build phases"))
347        self.targets = self.add_top(TopObjList("PBXNativeTarget"))
348        self.projects = self.add_top(TopObjList("PBXProject"))
349        self.configs = self.add_top(TopObjList("XCBuildConfiguration"))
350        self.config_lists = self.add_top(TopObjList("XCConfigurationList"))
351
352        self.group_main = self.add_group(Group())
353        self.group_products = self.add_group(Group(name="Products"))
354        self.group_main.children.add(self.group_products)
355
356        self.cfg_prod_release = self.add_config(Config("Release"))
357        self.cfg_prod_release.settings.pextend({"SDKROOT": "macosx",
358                                           "MACOSX_DEPLOYMENT_TARGET": "14.1",
359                                           })
360        self.proj_cfg_lst = self.add_cfg_lst(CfgList("proj config list"))
361        self.proj_cfg_lst.configs.add(self.cfg_prod_release)
362
363        self.root_proj = Project(self.proj_cfg_lst, self.group_main, self.group_products)
364        self.projects.add(self.root_proj)
365
366        self.test_exec = []
367
368    def add_top(self, t):
369        self.top_obj.append(t)
370        return t
371    def add_group(self, g):
372        self.groups.add(g)
373        return g
374    def add_build_phase(self, p):
375        self.build_phases.add(p)
376        return p
377    def add_config(self, c):
378        self.configs.add(c)
379        return c
380    def add_cfg_lst(self, c):
381        self.config_lists.add(c)
382        return c
383    def add_target(self, t):
384        self.targets.add(t)
385        return t
386
387    def add_xnu_archive(self):
388        f = File("libkernel.a", [])
389        fr = FileRef(f)
390        self.file_refs.add(fr)
391        self.group_products.children.add(fr)
392        self.xnu_phase_headers = self.add_build_phase(BuildPhase("PBXHeadersBuildPhase", "Headers"))
393        self.xnu_phase_sources = self.add_build_phase(BuildPhase("PBXSourcesBuildPhase", "Sources"))
394
395        cfg_xnu_release = self.add_config(Config("Release"))
396        cfg_xnu_release.settings.pextend( { "CODE_SIGN_STYLE": "Automatic",
397                                            "EXECUTABLE_PREFIX": "lib",
398                                            "PRODUCT_NAME": '"$(TARGET_NAME)"',
399                                            "SKIP_INSTALL": "YES"})
400        xnu_cfg_lst = self.add_cfg_lst(CfgList("target config list"))
401        xnu_cfg_lst.configs.add(cfg_xnu_release)
402
403        target = self.add_target(Target("xnu_static_lib", fr, xnu_cfg_lst, '"com.apple.product-type.library.static"'))
404        target.build_phases.extend([self.xnu_phase_headers, self.xnu_phase_sources])
405        self.root_proj.targets.add(target)
406
407    def add_test_target(self, c_file_ref, c_build_file):
408        name = os.path.splitext(os.path.split(c_file_ref.name)[1])[0]
409        f = File(name, [])
410        fr = FileRef(f)
411        self.file_refs.add(fr)
412        self.group_products.children.add(fr)
413        phase_h = self.add_build_phase(BuildPhase("PBXHeadersBuildPhase", "Headers"))
414        phase_src = self.add_build_phase(BuildPhase("PBXSourcesBuildPhase", "Sources"))
415        phase_src.files.add(c_build_file)
416
417        cfg_release = self.add_config(Config("Release"))
418        cfg_release.settings.pextend( { "CODE_SIGN_STYLE": "Automatic",
419                                        "PRODUCT_NAME": '"$(TARGET_NAME)"'})
420        cfg_lst = self.add_cfg_lst(CfgList("target config list"))
421        cfg_lst.configs.add(cfg_release)
422
423        target = self.add_target(Target(name, fr, cfg_lst, '"com.apple.product-type.tool"'))
424        target.build_phases.extend([phase_h, phase_src])
425        self.root_proj.targets.add(target)
426        self.test_exec.append(target)
427
428    def add_file(self, file_path, flags):
429        f = File(file_path, flags)
430        fr = FileRef(f)
431        bf = BuildFile(f)
432        self.build_files.add(bf)
433        self.file_refs.add(fr)
434        self.group_main.rec_add(file_path.split('/'), self.groups, fr)
435        typ = f.type_str()
436        if typ == TYPE_HEADER:
437            self.xnu_phase_headers.files.add(bf)
438        elif typ in [TYPE_SOURCE_C, TYPE_SOURCE_CPP, TYPE_SOURCE_ASM]:
439            self.xnu_phase_sources.files.add(bf)
440        return fr, bf
441    def add_ccj(self, ccj):
442        test_targets = []
443        for entry in ccj:
444            src_file, flags = parse_command(entry)
445            if src_file.endswith('dt_proxy.c'):
446                continue
447            fr, bf = self.add_file(src_file, flags)
448            if src_file.startswith(TESTS_UNIT_PREFIX):
449                test_targets.append((fr, bf))
450        test_targets.sort(key=lambda x:x[1].name)
451        for fr, bf in test_targets:
452            self.add_test_target(fr, bf)
453
454    def add_headers(self):
455        for path in pathlib.Path(SRC_ROOT).rglob('*.h'):
456            full_file = str(path)
457            assert full_file.startswith(SRC_ROOT), "unexpected path" + full_file
458            rel_file = full_file[len(SRC_ROOT)+1:]
459            self.add_file(str(rel_file), None)
460
461    def sort_groups(self):
462        self.group_main.sort()
463
464    def write(self, out):
465        out.write("// !$*UTF8*$!\n{\n")
466        out.write("\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 77;\n\tobjects = {\n\n")
467        for t in self.top_obj:
468            t.write(out, 2)
469        out.write(f"\t}};\n\trootObject = {self.root_proj.id};\n")
470        out.write("}")
471
472    def make_settings(self):
473        # go over all build files and find in their arguments a union of all the included folders
474        # this is useful for file navigation in xcode to work correctly
475        inc_dirs = set()
476        common_defines = None
477        for f in self.build_files.objs:
478            file_defines = set()
479            args = f.file.args
480            if args is None:
481                continue
482            for i, arg in enumerate(args):
483                if arg == '-I':
484                    d = args[i + 1]
485                    if d != ".":
486                        inc_dirs.add(args[i + 1])
487                elif arg == '-D':
488                    file_defines.add(args[i+1])
489            if common_defines is None:
490                common_defines = file_defines
491            else:
492                common_defines = common_defines.intersection(file_defines)
493        inc_str_lst = StrList.list_sort_quote(inc_dirs)
494        self.cfg_prod_release.settings.padd("HEADER_SEARCH_PATHS", inc_str_lst)
495        self.cfg_prod_release.settings.padd("SYSTEM_HEADER_SEARCH_PATHS", inc_str_lst)
496        str_common_defs = StrList.list_sort_quote(common_defines)
497        self.cfg_prod_release.settings.padd("GCC_PREPROCESSOR_DEFINITIONS", str_common_defs)
498
499    def write_schemes(self, folder, container_dir):
500        for target in self.test_exec:
501            path = os.path.join(folder, target.name + ".xcscheme")
502            out = open(path, "w")
503            exec_path = SRC_ROOT + "/" + TESTS_UNIT_BUILD_PREFIX + target.name
504            out.write(f'''<?xml version="1.0" encoding="UTF-8"?>
505<Scheme
506   LastUpgradeVersion = "1630"
507   version = "1.7">
508   <BuildAction
509      parallelizeBuildables = "YES"
510      buildImplicitDependencies = "YES"
511      buildArchitectures = "Automatic">
512      <BuildActionEntries>
513         <BuildActionEntry
514            buildForTesting = "NO"
515            buildForRunning = "NO"
516            buildForProfiling = "YES"
517            buildForArchiving = "NO"
518            buildForAnalyzing = "NO">
519            <BuildableReference
520               BuildableIdentifier = "primary"
521               BlueprintIdentifier = "{target.id}"
522               BuildableName = "{target.name}"
523               BlueprintName = "{target.name}"
524               ReferencedContainer = "container:{container_dir}">
525            </BuildableReference>
526         </BuildActionEntry>
527      </BuildActionEntries>
528   </BuildAction>
529   <LaunchAction
530      buildConfiguration = "Release"
531      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
532      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
533      launchStyle = "0"
534      useCustomWorkingDirectory = "NO"
535      ignoresPersistentStateOnLaunch = "NO"
536      debugDocumentVersioning = "YES"
537      debugServiceExtension = "internal"
538      allowLocationSimulation = "YES"
539      internalIOSLaunchStyle = "3"
540      viewDebuggingEnabled = "No">
541      <PathRunnable
542         runnableDebuggingMode = "0"
543         FilePath = "{exec_path}">
544      </PathRunnable>
545      <MacroExpansion>
546         <BuildableReference
547            BuildableIdentifier = "primary"
548            BlueprintIdentifier = "{target.id}"
549            BuildableName = "{target.name}"
550            BlueprintName = "{target.name}"
551            ReferencedContainer = "container:{container_dir}">
552         </BuildableReference>
553      </MacroExpansion>
554   </LaunchAction>
555</Scheme>
556''')
557            print(f"Wrote {path}")
558
559def gen_xcode(ccj):
560    p = PbxProj()
561    p.add_xnu_archive()
562    p.add_ccj(ccj)
563    p.add_headers()
564    p.sort_groups()
565    p.make_settings()
566
567    output = os.path.join(SRC_ROOT, "ut_xnu_proj.xcodeproj")
568    os.makedirs(output, exist_ok=True)
569    proj_path = os.path.join(output, "project.pbxproj")
570    p.write(open(proj_path, "w"))
571    print(f'wrote file: {proj_path};')
572
573    schemes_dir = output + "/xcshareddata/xcschemes"
574    os.makedirs(schemes_dir, exist_ok=True)
575    p.write_schemes(schemes_dir, output)
576    print(f'wrote schemes to: {schemes_dir}')
577
578# -------------------------------------- VSCode launch targets ----------------------------------------
579
580class TargetsProject:
581    def __init__(self):
582        self.targets = []
583
584    def add_ccj(self, ccj):
585        for entry in ccj:
586            src_file, flags = parse_command(entry)
587            if src_file.startswith(TESTS_UNIT_PREFIX):
588                name = os.path.splitext(src_file[len(TESTS_UNIT_PREFIX):])[0]
589                self.targets.append(name)
590        self.targets.sort()
591
592class VsCodeLaunchJson(TargetsProject):
593    def write(self, f):
594        confs = []
595        launch = {"version": "0.2.0", "configurations": confs }
596        for t in self.targets:
597            confs.append({
598                "name": t,
599                "type": "lldb-dap",
600                "request": "launch",
601                "program": "${workspaceFolder}/" + TESTS_UNIT_BUILD_PREFIX + t,
602                "stopOnEntry": False,
603                "cwd": "${workspaceFolder}",
604                "args": [],
605                "env": []
606            })
607        json.dump(launch, f, indent=4)
608
609
610def gen_vscode(ccj):
611    p = VsCodeLaunchJson()
612    p.add_ccj(ccj)
613
614    output = os.path.join(SRC_ROOT, ".vscode/launch.json")
615    os.makedirs(os.path.join(SRC_ROOT, ".vscode"), exist_ok=True)
616    if os.path.exists(output):
617        print(f"deleting existing {output}")
618        os.unlink(output)
619    p.write(open(output, "w"))
620    print(f"wrote {output}")
621
622# -------------------------------------- CLion targets ----------------------------------------
623
624def find_elem(root, tag, **kvarg):
625    assert len(kvarg.items()) == 1
626    key, val = list(kvarg.items())[0]
627    for child in root:
628        assert child.tag == tag, f'unexpected child.tag {child.tag}'
629        if child.attrib[key] == val:
630            return child
631    return None
632
633def get_elem(root, tag, **kvarg):
634    child = find_elem(root, tag, **kvarg)
635    key, val = list(kvarg.items())[0]
636    if child is not None:
637        return child, False
638    comp = ET.SubElement(root, tag)
639    comp.attrib[key] = val
640    return comp, True
641
642
643CLION_TOOLCHAIN_NAME = "System"
644class CLionProject(TargetsProject):
645    def _get_root(self, path):
646        if os.path.exists(path):
647            print(f"Parsing existing file {path}")
648            root = ET.parse(path).getroot()
649            assert root.tag == 'project', f'unexpected root.tag {root.tag}'
650        else:
651            root = ET.Element('project')
652            root.attrib["version"] = "4"
653        return root
654
655    def _write(self, root, path):
656        tree = ET.ElementTree(root)
657        ET.indent(tree, space='  ', level=0)
658        tree.write(open(path, "wb"), encoding="utf-8", xml_declaration=True)
659        print(f"Wrote {path}")
660
661    def make_custom_targets(self):
662        # add a target that uses toolchain "System"
663        path = os.path.join(SRC_ROOT, ".idea/customTargets.xml")
664        root = self._get_root(path)
665        comp, _ = get_elem(root, "component", name="CLionExternalBuildManager")
666        # check if we already have the target we need
667        for target in comp:
668            if target.attrib["defaultType"] == "TOOL":
669                target_name = target.attrib["name"]
670                if len(target) == 1 and target[0].tag == "configuration":
671                    conf = target[0]
672                    if conf.attrib["toolchainName"] == CLION_TOOLCHAIN_NAME:
673                        conf_name = conf.attrib["name"]
674                        print(f"file {path} already has the needed target with name {target_name},{conf_name}")
675                        return target_name, conf_name # it already exists, nothing to do
676        # add a new target
677        target_name = "test_default"
678        conf_name = "test_default"
679
680        target = ET.SubElement(comp, "target")
681        target.attrib["id"] = str(uuid.uuid1())
682        target.attrib["name"] = target_name
683        target.attrib["defaultType"] = "TOOL"
684
685        conf = ET.SubElement(target, "configuration")
686        conf.attrib["id"] = str(uuid.uuid1())
687        conf.attrib["name"] = conf_name
688        conf.attrib["toolchainName"] = CLION_TOOLCHAIN_NAME
689        print(f"Created target named {target_name}")
690        self._write(root, path)
691        return target_name, conf_name
692
693    def add_to_workspace(self, target_name, conf_name):
694        path = os.path.join(SRC_ROOT, ".idea/workspace.xml")
695        root = self._get_root(path)
696        comp, _ = get_elem(root, "component", name="RunManager")
697        added_anything = False
698        for t in self.targets:
699            for conf in comp:
700                if conf.tag != "configuration":
701                    continue
702                if conf.attrib["name"] == t:  # already has this target
703                    print(f"Found existing configuration named '{t}', not adding it")
704                    break
705            else:
706                print(f"Adding configuration for '{t}'")
707                proj_name = os.path.basename(SRC_ROOT)
708                conf = ET.SubElement(comp, "configuration", name=t,
709                                     type="CLionExternalRunConfiguration",
710                                     factoryName="Application",
711                                     REDIRECT_INPUT="false",
712                                     ELEVATE="false",
713                                     USE_EXTERNAL_CONSOLE="false",
714                                     EMULATE_TERMINAL="false",
715                                     PASS_PARENT_ENVS_2="true",
716                                     PROJECT_NAME=proj_name,
717                                     TARGET_NAME=target_name,
718                                     CONFIG_NAME=conf_name,
719                                     RUN_PATH=f"$PROJECT_DIR$/{TESTS_UNIT_BUILD_PREFIX}{t}")
720                ET.SubElement(conf, "method", v="2")
721                added_anything = True
722        if added_anything:
723            self._write(root, path)
724
725
726def gen_clion(ccj):
727    p = CLionProject()
728    p.add_ccj(ccj)
729
730    os.makedirs(os.path.join(SRC_ROOT, ".idea"), exist_ok=True)
731    target_name, conf_name = p.make_custom_targets()
732    p.add_to_workspace(target_name, conf_name)
733
734
735def main():
736    parser = argparse.ArgumentParser(description='Generate xcode project from compile_commands.json')
737    parser.add_argument('mode', help='IDE to generate for', choices=['xcode', 'vscode', 'clion'])
738    parser.add_argument('compile_commands', help='Path to compile_commands.json', nargs='*', default=os.path.join(SRC_ROOT, "compile_commands.json"))
739    args = parser.parse_args()
740
741    if not os.path.exists(args.compile_commands):
742        print(f"Can't find input {args.compile_commands}")
743        return 1
744
745    ccj = json.load(open(args.compile_commands, 'r'))
746
747    if args.mode == 'xcode':
748        return gen_xcode(ccj)
749    elif args.mode == 'vscode':
750        return gen_vscode(ccj)
751    elif args.mode == 'clion':
752        return gen_clion(ccj)
753
754
755if __name__ == '__main__':
756    main()
757
758