| # Copyright 2017 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| # |
| # gn_meta_sln.py |
| # Helper utility to combine GN-generated Visual Studio projects into |
| # a single meta-solution. |
| |
| |
| import os |
| import glob |
| import re |
| import sys |
| from shutil import copyfile |
| |
| # Helpers |
| def EnsureExists(path): |
| try: |
| os.makedirs(path) |
| except OSError: |
| pass |
| |
| def WriteLinesToFile(lines, file_name): |
| EnsureExists(os.path.dirname(file_name)) |
| with open(file_name, "w") as f: |
| f.writelines(lines) |
| |
| def ExtractIdg(proj_file_name): |
| result = [] |
| with open(proj_file_name) as proj_file: |
| lines = iter(proj_file) |
| for p_line in lines: |
| if "<ItemDefinitionGroup" in p_line: |
| while not "</ItemDefinitionGroup" in p_line: |
| result.append(p_line) |
| p_line = lines.next() |
| result.append(p_line) |
| return result |
| |
| # [ (name, solution_name, vs_version), ... ] |
| configs = [] |
| |
| def GetVSVersion(solution_file): |
| with open(solution_file) as f: |
| f.readline() |
| comment = f.readline().strip() |
| return comment[-4:] |
| |
| # Find all directories that can be used as configs (and record if they have VS |
| # files present) |
| for root, dirs, files in os.walk("out"): |
| for out_dir in dirs: |
| gn_file = os.path.join("out", out_dir, "build.ninja.d") |
| if os.path.exists(gn_file): |
| solutions = glob.glob(os.path.join("out", out_dir, "*.sln")) |
| for solution in solutions: |
| vs_version = GetVSVersion(solution) |
| configs.append((out_dir, os.path.basename(solution), |
| vs_version)) |
| break |
| |
| # Every project has a GUID that encodes the type. We only care about C++. |
| cpp_type_guid = "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942" |
| |
| # Work around MSBuild limitations by always using a fixed arch. |
| hard_coded_arch = "x64" |
| |
| # name -> [ (config, pathToProject, GUID, arch), ... ] |
| all_projects = {} |
| project_pattern = (r'Project\("\{' + cpp_type_guid + |
| r'\}"\) = "([^"]*)", "([^"]*)", "\{([^\}]*)\}"') |
| |
| # We need something to work with. Typically, this will fail if no GN folders |
| # have IDE files |
| if len(configs) == 0: |
| print("ERROR: At least one GN directory must have been built with --ide=vs") |
| sys.exit() |
| |
| # Filter out configs which don't match the name and vs version of the first. |
| name = configs[0][1] |
| vs_version = configs[0][2] |
| |
| for config in configs: |
| if config[1] != name or config[2] != vs_version: |
| continue |
| |
| sln_lines = iter(open(os.path.join("out", config[0], config[1]))) |
| for sln_line in sln_lines: |
| match_obj = re.match(project_pattern, sln_line) |
| if match_obj: |
| proj_name = match_obj.group(1) |
| if proj_name not in all_projects: |
| all_projects[proj_name] = [] |
| all_projects[proj_name].append((config[0], match_obj.group(2), |
| match_obj.group(3))) |
| |
| # We need something to work with. Typically, this will fail if no GN folders |
| # have IDE files |
| if len(all_projects) == 0: |
| print("ERROR: At least one GN directory must have been built with --ide=vs") |
| sys.exit() |
| |
| # Create a new solution. We arbitrarily use the first config as the GUID source |
| # (but we need to match that behavior later, when we copy/generate the project |
| # files). |
| new_sln_lines = [] |
| new_sln_lines.append( |
| 'Microsoft Visual Studio Solution File, Format Version 12.00\n') |
| new_sln_lines.append('# Visual Studio ' + vs_version + '\n') |
| for proj_name, proj_configs in all_projects.items(): |
| new_sln_lines.append('Project("{' + cpp_type_guid + '}") = "' + proj_name + |
| '", "' + proj_configs[0][1] + '", "{' + |
| proj_configs[0][2] + '}"\n') |
| new_sln_lines.append('EndProject\n') |
| |
| new_sln_lines.append('Global\n') |
| new_sln_lines.append( |
| '\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n') |
| for config in configs: |
| match = config[0] + '|' + hard_coded_arch |
| new_sln_lines.append('\t\t' + match + ' = ' + match + '\n') |
| new_sln_lines.append('\tEndGlobalSection\n') |
| new_sln_lines.append( |
| '\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n') |
| for proj_name, proj_configs in all_projects.items(): |
| proj_guid = proj_configs[0][2] |
| for config in configs: |
| match = config[0] + '|' + hard_coded_arch |
| new_sln_lines.append('\t\t{' + proj_guid + '}.' + match + |
| '.ActiveCfg = ' + match + '\n') |
| new_sln_lines.append('\t\t{' + proj_guid + '}.' + match + |
| '.Build.0 = ' + match + '\n') |
| new_sln_lines.append('\tEndGlobalSection\n') |
| new_sln_lines.append('\tGlobalSection(SolutionProperties) = preSolution\n') |
| new_sln_lines.append('\t\tHideSolutionNode = FALSE\n') |
| new_sln_lines.append('\tEndGlobalSection\n') |
| new_sln_lines.append('\tGlobalSection(NestedProjects) = preSolution\n') |
| new_sln_lines.append('\tEndGlobalSection\n') |
| new_sln_lines.append('EndGlobal\n') |
| |
| # Write solution file |
| WriteLinesToFile(new_sln_lines, 'out/sln/' + name) |
| |
| idg_hdr = "<ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='" |
| |
| configuration_template = """ <ProjectConfiguration Include="{config}|{arch}"> |
| <Configuration>{config}</Configuration> |
| <Platform>{arch}</Platform> |
| </ProjectConfiguration> |
| """ |
| |
| def FormatProjectConfig(config): |
| return configuration_template.format( |
| config = config[0], arch = hard_coded_arch) |
| |
| # Now, bring over the project files |
| for proj_name, proj_configs in all_projects.items(): |
| # Paths to project and filter file in src and dst locations |
| src_proj_path = os.path.join("out", proj_configs[0][0], proj_configs[0][1]) |
| dst_proj_path = os.path.join("out", "sln", proj_configs[0][1]) |
| src_filter_path = src_proj_path + ".filters" |
| dst_filter_path = dst_proj_path + ".filters" |
| |
| # Copy the filter file unmodified |
| EnsureExists(os.path.dirname(dst_proj_path)) |
| copyfile(src_filter_path, dst_filter_path) |
| |
| preferred_tool_arch = None |
| config_arch = {} |
| |
| # Bring over the project file, modified with extra configs |
| with open(src_proj_path) as src_proj_file: |
| proj_lines = iter(src_proj_file) |
| new_proj_lines = [] |
| for line in proj_lines: |
| if "<ItemDefinitionGroup" in line: |
| # This is a large group that contains many settings. We need to |
| # replicate it, with conditions so it varies per configuration. |
| idg_lines = [] |
| while not "</ItemDefinitionGroup" in line: |
| idg_lines.append(line) |
| line = proj_lines.next() |
| idg_lines.append(line) |
| for proj_config in proj_configs: |
| config_idg_lines = ExtractIdg(os.path.join("out", |
| proj_config[0], |
| proj_config[1])) |
| match = proj_config[0] + '|' + hard_coded_arch |
| new_proj_lines.append(idg_hdr + match + "'\">\n") |
| for idg_line in config_idg_lines[1:]: |
| new_proj_lines.append(idg_line) |
| elif "ProjectConfigurations" in line: |
| new_proj_lines.append(line) |
| proj_lines.next() |
| proj_lines.next() |
| proj_lines.next() |
| proj_lines.next() |
| for config in configs: |
| new_proj_lines.append(FormatProjectConfig(config)) |
| |
| elif "<OutDir" in line: |
| new_proj_lines.append(line.replace(proj_configs[0][0], |
| "$(Configuration)")) |
| elif "<PreferredToolArchitecture" in line: |
| new_proj_lines.append(" <PreferredToolArchitecture>" + |
| hard_coded_arch + |
| "</PreferredToolArchitecture>\n") |
| else: |
| new_proj_lines.append(line) |
| with open(dst_proj_path, "w") as new_proj: |
| new_proj.writelines(new_proj_lines) |
| |
| print('Wrote meta solution to out/sln/' + name) |