gn_meta_sln.py (8314B)
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 10 import os 11 import glob 12 import re 13 import sys 14 from shutil import copyfile 15 16 # Helpers 17 def EnsureExists(path): 18 try: 19 os.makedirs(path) 20 except OSError: 21 pass 22 23 def WriteLinesToFile(lines, file_name): 24 EnsureExists(os.path.dirname(file_name)) 25 with open(file_name, "w") as f: 26 f.writelines(lines) 27 28 def 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), ... ] 41 configs = [] 42 43 def 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) 51 for 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++. 63 cpp_type_guid = "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942" 64 65 # Work around MSBuild limitations by always using a fixed arch. 66 hard_coded_arch = "x64" 67 68 # name -> [ (config, pathToProject, GUID, arch), ... ] 69 all_projects = {} 70 project_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 75 if 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. 80 name = configs[0][1] 81 vs_version = configs[0][2] 82 83 for 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 99 if 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). 106 new_sln_lines = [] 107 new_sln_lines.append( 108 'Microsoft Visual Studio Solution File, Format Version 12.00\n') 109 new_sln_lines.append('# Visual Studio ' + vs_version + '\n') 110 for 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 116 new_sln_lines.append('Global\n') 117 new_sln_lines.append( 118 '\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n') 119 for config in configs: 120 match = config[0] + '|' + hard_coded_arch 121 new_sln_lines.append('\t\t' + match + ' = ' + match + '\n') 122 new_sln_lines.append('\tEndGlobalSection\n') 123 new_sln_lines.append( 124 '\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n') 125 for 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') 133 new_sln_lines.append('\tEndGlobalSection\n') 134 new_sln_lines.append('\tGlobalSection(SolutionProperties) = preSolution\n') 135 new_sln_lines.append('\t\tHideSolutionNode = FALSE\n') 136 new_sln_lines.append('\tEndGlobalSection\n') 137 new_sln_lines.append('\tGlobalSection(NestedProjects) = preSolution\n') 138 new_sln_lines.append('\tEndGlobalSection\n') 139 new_sln_lines.append('EndGlobal\n') 140 141 # Write solution file 142 WriteLinesToFile(new_sln_lines, 'out/sln/' + name) 143 144 idg_hdr = "<ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='" 145 146 configuration_template = """ <ProjectConfiguration Include="{config}|{arch}"> 147 <Configuration>{config}</Configuration> 148 <Platform>{arch}</Platform> 149 </ProjectConfiguration> 150 """ 151 152 def FormatProjectConfig(config): 153 return configuration_template.format( 154 config = config[0], arch = hard_coded_arch) 155 156 # Now, bring over the project files 157 for 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 213 print('Wrote meta solution to out/sln/' + name)