• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2017 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4#
5# gn_meta_sln.py
6#   Helper utility to combine GN-generated Visual Studio projects into
7#   a single meta-solution.
8
9
10import os
11import glob
12import re
13import sys
14from shutil import copyfile
15
16# Helpers
17def EnsureExists(path):
18    try:
19        os.makedirs(path)
20    except OSError:
21        pass
22
23def WriteLinesToFile(lines, file_name):
24    EnsureExists(os.path.dirname(file_name))
25    with open(file_name, "w") as f:
26        f.writelines(lines)
27
28def ExtractIdg(proj_file_name):
29    result = []
30    with open(proj_file_name) as proj_file:
31        lines = iter(proj_file)
32        for p_line in lines:
33            if "<ItemDefinitionGroup" in p_line:
34                while not "</ItemDefinitionGroup" in p_line:
35                    result.append(p_line)
36                    p_line = lines.next()
37                result.append(p_line)
38                return result
39
40# [ (name, solution_name, vs_version), ... ]
41configs = []
42
43def GetVSVersion(solution_file):
44    with open(solution_file) as f:
45        f.readline()
46        comment = f.readline().strip()
47        return comment[-4:]
48
49# Find all directories that can be used as configs (and record if they have VS
50# files present)
51for root, dirs, files in os.walk("out"):
52    for out_dir in dirs:
53        gn_file = os.path.join("out", out_dir, "build.ninja.d")
54        if os.path.exists(gn_file):
55            solutions = glob.glob(os.path.join("out", out_dir, "*.sln"))
56            for solution in solutions:
57                vs_version = GetVSVersion(solution)
58                configs.append((out_dir, os.path.basename(solution),
59                                vs_version))
60    break
61
62# Every project has a GUID that encodes the type. We only care about C++.
63cpp_type_guid = "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942"
64
65# Work around MSBuild limitations by always using a fixed arch.
66hard_coded_arch = "x64"
67
68# name -> [ (config, pathToProject, GUID, arch), ... ]
69all_projects = {}
70project_pattern = (r'Project\("\{' + cpp_type_guid +
71                   r'\}"\) = "([^"]*)", "([^"]*)", "\{([^\}]*)\}"')
72
73# We need something to work with. Typically, this will fail if no GN folders
74# have IDE files
75if len(configs) == 0:
76    print("ERROR: At least one GN directory must have been built with --ide=vs")
77    sys.exit()
78
79# Filter out configs which don't match the name and vs version of the first.
80name = configs[0][1]
81vs_version = configs[0][2]
82
83for config in configs:
84    if config[1] != name or config[2] != vs_version:
85        continue
86
87    sln_lines = iter(open(os.path.join("out", config[0], config[1])))
88    for sln_line in sln_lines:
89        match_obj = re.match(project_pattern, sln_line)
90        if match_obj:
91            proj_name = match_obj.group(1)
92            if proj_name not in all_projects:
93                all_projects[proj_name] = []
94            all_projects[proj_name].append((config[0], match_obj.group(2),
95                                            match_obj.group(3)))
96
97# We need something to work with. Typically, this will fail if no GN folders
98# have IDE files
99if len(all_projects) == 0:
100    print("ERROR: At least one GN directory must have been built with --ide=vs")
101    sys.exit()
102
103# Create a new solution. We arbitrarily use the first config as the GUID source
104# (but we need to match that behavior later, when we copy/generate the project
105# files).
106new_sln_lines = []
107new_sln_lines.append(
108    'Microsoft Visual Studio Solution File, Format Version 12.00\n')
109new_sln_lines.append('# Visual Studio ' + vs_version + '\n')
110for proj_name, proj_configs in all_projects.items():
111    new_sln_lines.append('Project("{' + cpp_type_guid + '}") = "' + proj_name +
112                         '", "' + proj_configs[0][1] + '", "{' +
113                         proj_configs[0][2] + '}"\n')
114    new_sln_lines.append('EndProject\n')
115
116new_sln_lines.append('Global\n')
117new_sln_lines.append(
118    '\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n')
119for config in configs:
120    match = config[0] + '|' + hard_coded_arch
121    new_sln_lines.append('\t\t' + match + ' = ' + match + '\n')
122new_sln_lines.append('\tEndGlobalSection\n')
123new_sln_lines.append(
124    '\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n')
125for proj_name, proj_configs in all_projects.items():
126    proj_guid = proj_configs[0][2]
127    for config in configs:
128        match = config[0] + '|' + hard_coded_arch
129        new_sln_lines.append('\t\t{' + proj_guid + '}.' + match +
130                           '.ActiveCfg = ' + match + '\n')
131        new_sln_lines.append('\t\t{' + proj_guid + '}.' + match +
132                           '.Build.0 = ' + match + '\n')
133new_sln_lines.append('\tEndGlobalSection\n')
134new_sln_lines.append('\tGlobalSection(SolutionProperties) = preSolution\n')
135new_sln_lines.append('\t\tHideSolutionNode = FALSE\n')
136new_sln_lines.append('\tEndGlobalSection\n')
137new_sln_lines.append('\tGlobalSection(NestedProjects) = preSolution\n')
138new_sln_lines.append('\tEndGlobalSection\n')
139new_sln_lines.append('EndGlobal\n')
140
141# Write solution file
142WriteLinesToFile(new_sln_lines, 'out/sln/' + name)
143
144idg_hdr = "<ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='"
145
146configuration_template = """    <ProjectConfiguration Include="{config}|{arch}">
147      <Configuration>{config}</Configuration>
148      <Platform>{arch}</Platform>
149    </ProjectConfiguration>
150"""
151
152def FormatProjectConfig(config):
153    return configuration_template.format(
154        config = config[0], arch = hard_coded_arch)
155
156# Now, bring over the project files
157for proj_name, proj_configs in all_projects.items():
158    # Paths to project and filter file in src and dst locations
159    src_proj_path = os.path.join("out", proj_configs[0][0], proj_configs[0][1])
160    dst_proj_path = os.path.join("out", "sln", proj_configs[0][1])
161    src_filter_path = src_proj_path + ".filters"
162    dst_filter_path = dst_proj_path + ".filters"
163
164    # Copy the filter file unmodified
165    EnsureExists(os.path.dirname(dst_proj_path))
166    copyfile(src_filter_path, dst_filter_path)
167
168    preferred_tool_arch = None
169    config_arch = {}
170
171    # Bring over the project file, modified with extra configs
172    with open(src_proj_path) as src_proj_file:
173        proj_lines = iter(src_proj_file)
174        new_proj_lines = []
175        for line in proj_lines:
176            if "<ItemDefinitionGroup" in line:
177                # This is a large group that contains many settings. We need to
178                # replicate it, with conditions so it varies per configuration.
179                idg_lines = []
180                while not "</ItemDefinitionGroup" in line:
181                    idg_lines.append(line)
182                    line = proj_lines.next()
183                idg_lines.append(line)
184                for proj_config in proj_configs:
185                    config_idg_lines = ExtractIdg(os.path.join("out",
186                                                             proj_config[0],
187                                                             proj_config[1]))
188                    match = proj_config[0] + '|' + hard_coded_arch
189                    new_proj_lines.append(idg_hdr + match + "'\">\n")
190                    for idg_line in config_idg_lines[1:]:
191                        new_proj_lines.append(idg_line)
192            elif "ProjectConfigurations" in line:
193                new_proj_lines.append(line)
194                proj_lines.next()
195                proj_lines.next()
196                proj_lines.next()
197                proj_lines.next()
198                for config in configs:
199                    new_proj_lines.append(FormatProjectConfig(config))
200
201            elif "<OutDir" in line:
202                new_proj_lines.append(line.replace(proj_configs[0][0],
203                                                 "$(Configuration)"))
204            elif "<PreferredToolArchitecture" in line:
205                new_proj_lines.append("    <PreferredToolArchitecture>" +
206                                      hard_coded_arch +
207                                      "</PreferredToolArchitecture>\n")
208            else:
209                new_proj_lines.append(line)
210        with open(dst_proj_path, "w") as new_proj:
211            new_proj.writelines(new_proj_lines)
212
213print('Wrote meta solution to out/sln/' + name)
214