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