Source code

Revision control

Copy as Markdown

Other Tools

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This file contains a build backend for generating Visual Studio project
# files.
import errno
import os
import re
import uuid
from pathlib import Path
from xml.dom import getDOMImplementation
from mozpack.files import FileFinder
from mozbuild.base import ExecutionSummary
from ..frontend.data import (
Defines,
HostProgram,
HostSources,
Library,
LocalInclude,
Program,
SandboxedWasmLibrary,
Sources,
UnifiedSources,
)
from .common import CommonBackend
def get_id(name):
return str(uuid.uuid5(uuid.NAMESPACE_URL, name)).upper()
def visual_studio_product_to_solution_version(version):
if version == "2017":
return "12.00", "15"
elif version == "2019":
return "12.00", "16"
elif version == "2022":
return "12.00", "17"
else:
raise Exception("Unknown version seen: %s" % version)
def visual_studio_product_to_platform_toolset_version(version):
if version == "2017":
return "v141"
elif version == "2019":
return "v142"
elif version == "2022":
return "v143"
else:
raise Exception("Unknown version seen: %s" % version)
class VisualStudioBackend(CommonBackend):
"""Generate Visual Studio project files.
This backend is used to produce Visual Studio projects and a solution
to foster developing Firefox with Visual Studio.
This backend is currently considered experimental. There are many things
not optimal about how it works.
"""
def _init(self):
CommonBackend._init(self)
# These should eventually evolve into parameters.
self._out_dir = os.path.join(self.environment.topobjdir, "msvc")
self._projsubdir = "projects"
self._version = self.environment.substs.get("MSVS_VERSION", "2017")
self._paths_to_sources = {}
self._paths_to_includes = {}
self._paths_to_defines = {}
self._paths_to_configs = {}
self._libs_to_paths = {}
self._progs_to_paths = {}
def summary(self):
return ExecutionSummary(
"VisualStudio backend executed in {execution_time:.2f}s\n"
"Generated Visual Studio solution at {path:s}",
execution_time=self._execution_time,
path=os.path.join(self._out_dir, "mozilla.sln"),
)
def consume_object(self, obj):
reldir = getattr(obj, "relsrcdir", None)
if hasattr(obj, "config") and reldir not in self._paths_to_configs:
self._paths_to_configs[reldir] = obj.config
if isinstance(obj, Sources):
self._add_sources(reldir, obj)
elif isinstance(obj, HostSources):
self._add_sources(reldir, obj)
elif isinstance(obj, UnifiedSources):
# XXX we should be letting CommonBackend.consume_object call this
# for us instead.
self._process_unified_sources(obj)
elif isinstance(obj, Library) and not isinstance(obj, SandboxedWasmLibrary):
self._libs_to_paths[obj.basename] = reldir
elif isinstance(obj, Program) or isinstance(obj, HostProgram):
self._progs_to_paths[obj.program] = reldir
elif isinstance(obj, Defines):
self._paths_to_defines.setdefault(reldir, {}).update(obj.defines)
elif isinstance(obj, LocalInclude):
includes = self._paths_to_includes.setdefault(reldir, [])
includes.append(obj.path.full_path)
# Just acknowledge everything.
return True
def _add_sources(self, reldir, obj):
s = self._paths_to_sources.setdefault(reldir, set())
s.update(obj.files)
def _process_unified_sources(self, obj):
reldir = getattr(obj, "relsrcdir", None)
s = self._paths_to_sources.setdefault(reldir, set())
s.update(obj.files)
def consume_finished(self):
out_dir = self._out_dir
out_proj_dir = os.path.join(self._out_dir, self._projsubdir)
projects = self._write_projects_for_sources(
self._libs_to_paths, "library", out_proj_dir
)
projects.update(
self._write_projects_for_sources(
self._progs_to_paths, "binary", out_proj_dir
)
)
# Generate projects that can be used to build common targets.
for target in ("export", "binaries", "tools", "full"):
basename = "target_%s" % target
command = "$(SolutionDir)\\mach.bat build"
if target != "full":
command += " %s" % target
project_id = self._write_vs_project(
out_proj_dir,
basename,
target,
build_command=command,
clean_command="$(SolutionDir)\\mach.bat clobber",
)
projects[basename] = (project_id, basename, target)
# A project that can be used to regenerate the visual studio projects.
basename = "target_vs"
project_id = self._write_vs_project(
out_proj_dir,
basename,
"visual-studio",
build_command="$(SolutionDir)\\mach.bat build-backend -b VisualStudio",
)
projects[basename] = (project_id, basename, "visual-studio")
# Write out a shared property file with common variables.
props_path = os.path.join(out_proj_dir, "mozilla.props")
with self._write_file(props_path, readmode="rb") as fh:
self._write_props(fh)
# Generate some wrapper scripts that allow us to invoke mach inside
# a MozillaBuild-like environment. We currently only use the batch
# script. We'd like to use the PowerShell script. However, it seems
# to buffer output from within Visual Studio (surely this is
# configurable) and the default execution policy of PowerShell doesn't
# allow custom scripts to be executed.
with self._write_file(os.path.join(out_dir, "mach.bat"), readmode="rb") as fh:
self._write_mach_batch(fh)
with self._write_file(os.path.join(out_dir, "mach.ps1"), readmode="rb") as fh:
self._write_mach_powershell(fh)
# Write out a solution file to tie it all together.
solution_path = os.path.join(out_dir, "mozilla.sln")
with self._write_file(solution_path, readmode="rb") as fh:
self._write_solution(fh, projects)
def _write_projects_for_sources(self, sources, prefix, out_dir):
projects = {}
for item, path in sorted(sources.items()):
config = self._paths_to_configs.get(path, None)
sources = self._paths_to_sources.get(path, set())
sources = set(os.path.join("$(TopSrcDir)", path, s) for s in sources)
sources = set(os.path.normpath(s) for s in sources)
finder = FileFinder(os.path.join(self.environment.topsrcdir, path))
headers = [t[0] for t in finder.find("*.h")]
headers = [
os.path.normpath(os.path.join("$(TopSrcDir)", path, f)) for f in headers
]
includes = [
os.path.join("$(TopSrcDir)", path),
os.path.join("$(TopObjDir)", path),
]
includes.extend(self._paths_to_includes.get(path, []))
includes.append("$(TopObjDir)\\dist\\include\\nss")
includes.append("$(TopObjDir)\\dist\\include")
for v in (
"NSPR_CFLAGS",
"NSS_CFLAGS",
"MOZ_JPEG_CFLAGS",
"MOZ_PNG_CFLAGS",
"MOZ_ZLIB_CFLAGS",
"MOZ_PIXMAN_CFLAGS",
):
if not config:
break
args = config.substs.get(v, [])
for i, arg in enumerate(args):
if arg.startswith("-I"):
includes.append(os.path.normpath(arg[2:]))
# Pull in system defaults.
includes.append("$(DefaultIncludes)")
includes = [os.path.normpath(i) for i in includes]
defines = []
for k, v in self._paths_to_defines.get(path, {}).items():
if v is True:
defines.append(k)
else:
defines.append("%s=%s" % (k, v))
debugger = None
if prefix == "binary":
if item.startswith(self.environment.substs["MOZ_APP_NAME"]):
app_args = "-no-remote -profile $(TopObjDir)\\tmp\\profile-default"
if self.environment.substs.get("MOZ_LAUNCHER_PROCESS", False):
app_args += " -wait-for-browser"
debugger = ("$(TopObjDir)\\dist\\bin\\%s" % item, app_args)
else:
debugger = ("$(TopObjDir)\\dist\\bin\\%s" % item, "")
basename = "%s_%s" % (prefix, item)
project_id = self._write_vs_project(
out_dir,
basename,
item,
includes=includes,
forced_includes=["$(TopObjDir)\\dist\\include\\mozilla-config.h"],
defines=defines,
headers=headers,
sources=sources,
debugger=debugger,
)
projects[basename] = (project_id, basename, item)
return projects
def _write_solution(self, fh, projects):
# Visual Studio appears to write out its current version in the
# solution file. Instead of trying to figure out what version it will
# write, try to parse the version out of the existing file and use it
# verbatim.
vs_version = None
try:
with open(fh.name, "rb") as sfh:
for line in sfh:
if line.startswith(b"VisualStudioVersion = "):
vs_version = line.split(b" = ", 1)[1].strip()
except IOError as e:
if e.errno != errno.ENOENT:
raise
format_version, comment_version = visual_studio_product_to_solution_version(
self._version
)
# This is a Visual C++ Project type.
project_type = "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942"
# Visual Studio seems to require this header.
fh.write(
"Microsoft Visual Studio Solution File, Format Version %s\r\n"
% format_version
)
fh.write("# Visual Studio %s\r\n" % comment_version)
if vs_version:
fh.write("VisualStudioVersion = %s\r\n" % vs_version)
# Corresponds to VS2013.
fh.write("MinimumVisualStudioVersion = 12.0.31101.0\r\n")
binaries_id = projects["target_binaries"][0]
# Write out entries for each project.
for key in sorted(projects):
project_id, basename, name = projects[key]
path = os.path.join(self._projsubdir, "%s.vcxproj" % basename)
fh.write(
'Project("{%s}") = "%s", "%s", "{%s}"\r\n'
% (project_type, name, path, project_id)
)
# Make all libraries depend on the binaries target.
if key.startswith("library_"):
fh.write("\tProjectSection(ProjectDependencies) = postProject\r\n")
fh.write("\t\t{%s} = {%s}\r\n" % (binaries_id, binaries_id))
fh.write("\tEndProjectSection\r\n")
fh.write("EndProject\r\n")
# Write out solution folders for organizing things.
# This is the UUID you use for solution folders.
container_id = "2150E333-8FDC-42A3-9474-1A3956D46DE8"
def write_container(desc):
cid = get_id(desc)
fh.write(
'Project("{%s}") = "%s", "%s", "{%s}"\r\n'
% (container_id, desc, desc, cid)
)
fh.write("EndProject\r\n")
return cid
library_id = write_container("Libraries")
target_id = write_container("Build Targets")
binary_id = write_container("Binaries")
fh.write("Global\r\n")
# Make every project a member of our one configuration.
fh.write("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n")
fh.write("\t\tBuild|Win32 = Build|Win32\r\n")
fh.write("\tEndGlobalSection\r\n")
# Set every project's active configuration to the one configuration and
# set up the default build project.
fh.write("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n")
for name, project in sorted(projects.items()):
fh.write("\t\t{%s}.Build|Win32.ActiveCfg = Build|Win32\r\n" % project[0])
# Only build the full build target by default.
# It's important we don't write multiple entries here because they
# conflict!
if name == "target_full":
fh.write("\t\t{%s}.Build|Win32.Build.0 = Build|Win32\r\n" % project[0])
fh.write("\tEndGlobalSection\r\n")
fh.write("\tGlobalSection(SolutionProperties) = preSolution\r\n")
fh.write("\t\tHideSolutionNode = FALSE\r\n")
fh.write("\tEndGlobalSection\r\n")
# Associate projects with containers.
fh.write("\tGlobalSection(NestedProjects) = preSolution\r\n")
for key in sorted(projects):
project_id = projects[key][0]
if key.startswith("library_"):
container_id = library_id
elif key.startswith("target_"):
container_id = target_id
elif key.startswith("binary_"):
container_id = binary_id
else:
raise Exception("Unknown project type: %s" % key)
fh.write("\t\t{%s} = {%s}\r\n" % (project_id, container_id))
fh.write("\tEndGlobalSection\r\n")
fh.write("EndGlobal\r\n")
def _write_props(self, fh):
impl = getDOMImplementation()
doc = impl.createDocument(MSBUILD_NAMESPACE, "Project", None)
project = doc.documentElement
project.setAttribute("xmlns", MSBUILD_NAMESPACE)
project.setAttribute("ToolsVersion", "4.0")
ig = project.appendChild(doc.createElement("ImportGroup"))
ig.setAttribute("Label", "PropertySheets")
pg = project.appendChild(doc.createElement("PropertyGroup"))
pg.setAttribute("Label", "UserMacros")
ig = project.appendChild(doc.createElement("ItemGroup"))
def add_var(k, v):
e = pg.appendChild(doc.createElement(k))
e.appendChild(doc.createTextNode(v))
e = ig.appendChild(doc.createElement("BuildMacro"))
e.setAttribute("Include", k)
e = e.appendChild(doc.createElement("Value"))
e.appendChild(doc.createTextNode("$(%s)" % k))
natvis = ig.appendChild(doc.createElement("Natvis"))
natvis.setAttribute("Include", "../../../toolkit/library/gecko.natvis")
add_var("TopObjDir", os.path.normpath(self.environment.topobjdir))
add_var("TopSrcDir", os.path.normpath(self.environment.topsrcdir))
add_var("PYTHON", "$(TopObjDir)\\_virtualenv\\Scripts\\python.exe")
add_var("MACH", "$(TopSrcDir)\\mach")
# From MozillaBuild.
add_var("DefaultIncludes", os.environ.get("INCLUDE", ""))
fh.write(b"\xef\xbb\xbf")
doc.writexml(fh, addindent=" ", newl="\r\n")
def _create_natvis_type(
self, doc, visualizer, name, displayString, stringView=None
):
t = visualizer.appendChild(doc.createElement("Type"))
t.setAttribute("Name", name)
ds = t.appendChild(doc.createElement("DisplayString"))
ds.appendChild(doc.createTextNode(displayString))
if stringView is not None:
sv = t.appendChild(doc.createElement("DisplayString"))
sv.appendChild(doc.createTextNode(stringView))
def _create_natvis_simple_string_type(self, doc, visualizer, name):
self._create_natvis_type(
doc, visualizer, name + "<char16_t>", "{mData,su}", "mData,su"
)
self._create_natvis_type(
doc, visualizer, name + "<char>", "{mData,s}", "mData,s"
)
def _create_natvis_string_tuple_type(self, doc, visualizer, chartype, formatstring):
t = visualizer.appendChild(doc.createElement("Type"))
t.setAttribute("Name", "nsTSubstringTuple<" + chartype + ">")
ds1 = t.appendChild(doc.createElement("DisplayString"))
ds1.setAttribute("Condition", "mHead != nullptr")
ds1.appendChild(
doc.createTextNode("{mHead,na} {mFragB->mData," + formatstring + "}")
)
ds2 = t.appendChild(doc.createElement("DisplayString"))
ds2.setAttribute("Condition", "mHead == nullptr")
ds2.appendChild(
doc.createTextNode(
"{mFragA->mData,"
+ formatstring
+ "} {mFragB->mData,"
+ formatstring
+ "}"
)
)
def _relevant_environment_variables(self):
# Write out the environment variables, presumably coming from
# MozillaBuild.
for k, v in sorted(os.environ.items()):
if not re.match("^[a-zA-Z0-9_]+$", k):
continue
if k in ("OLDPWD", "PS1"):
continue
if k.startswith("_"):
continue
yield k, v
yield "TOPSRCDIR", self.environment.topsrcdir
yield "TOPOBJDIR", self.environment.topobjdir
def _write_mach_powershell(self, fh):
for k, v in self._relevant_environment_variables():
fh.write(b'$env:%s = "%s"\r\n' % (k.encode("utf-8"), v.encode("utf-8")))
relpath = os.path.relpath(
self.environment.topsrcdir, self.environment.topobjdir
).replace("\\", "/")
fh.write(
b'$bashargs = "%s/mach", "--log-no-times"\r\n' % relpath.encode("utf-8")
)
fh.write(b"$bashargs = $bashargs + $args\r\n")
fh.write(b"$expanded = $bashargs -join ' '\r\n")
fh.write(b'$procargs = "-c", $expanded\r\n')
if (Path(os.environ["MOZILLABUILD"]) / "msys2").exists():
bash_path = rb"msys2\usr\bin\bash"
else:
bash_path = rb"msys\bin\bash"
fh.write(
b"Start-Process -WorkingDirectory $env:TOPOBJDIR "
b"-FilePath $env:MOZILLABUILD\\%b "
b"-ArgumentList $procargs "
b"-Wait -NoNewWindow\r\n" % bash_path
)
def _write_mach_batch(self, fh):
"""Write out a batch script that builds the tree.
The script "bootstraps" into the MozillaBuild environment by setting
the environment variables that are active in the current MozillaBuild
environment. Then, it builds the tree.
"""
for k, v in self._relevant_environment_variables():
fh.write(b'SET "%s=%s"\r\n' % (k.encode("utf-8"), v.encode("utf-8")))
fh.write(b"cd %TOPOBJDIR%\r\n")
# We need to convert Windows-native paths to msys paths. Easiest way is
# relative paths, since munging c:\ to /c/ is slightly more
# complicated.
relpath = os.path.relpath(
self.environment.topsrcdir, self.environment.topobjdir
).replace("\\", "/")
if (Path(os.environ["MOZILLABUILD"]) / "msys2").exists():
bash_path = rb"msys2\usr\bin\bash"
else:
bash_path = rb"msys\bin\bash"
# We go through mach because it has the logic for choosing the most
# appropriate build tool.
fh.write(
b'"%%MOZILLABUILD%%\\%b" '
b'-c "%s/mach --log-no-times %%1 %%2 %%3 %%4 %%5 %%6 %%7"'
% (bash_path, relpath.encode("utf-8"))
)
def _write_vs_project(self, out_dir, basename, name, **kwargs):
root = "%s.vcxproj" % basename
project_id = get_id(basename)
with self._write_file(os.path.join(out_dir, root), readmode="rb") as fh:
project_id, name = VisualStudioBackend.write_vs_project(
fh, self._version, project_id, name, **kwargs
)
with self._write_file(
os.path.join(out_dir, "%s.user" % root), readmode="rb"
) as fh:
fh.write('<?xml version="1.0" encoding="utf-8"?>\r\n')
fh.write('<Project ToolsVersion="4.0" xmlns="%s">\r\n' % MSBUILD_NAMESPACE)
fh.write("</Project>\r\n")
return project_id
@staticmethod
def write_vs_project(
fh,
version,
project_id,
name,
includes=[],
forced_includes=[],
defines=[],
build_command=None,
clean_command=None,
debugger=None,
headers=[],
sources=[],
):
impl = getDOMImplementation()
doc = impl.createDocument(MSBUILD_NAMESPACE, "Project", None)
project = doc.documentElement
project.setAttribute("DefaultTargets", "Build")
project.setAttribute("ToolsVersion", "4.0")
project.setAttribute("xmlns", MSBUILD_NAMESPACE)
ig = project.appendChild(doc.createElement("ItemGroup"))
ig.setAttribute("Label", "ProjectConfigurations")
pc = ig.appendChild(doc.createElement("ProjectConfiguration"))
pc.setAttribute("Include", "Build|Win32")
c = pc.appendChild(doc.createElement("Configuration"))
c.appendChild(doc.createTextNode("Build"))
p = pc.appendChild(doc.createElement("Platform"))
p.appendChild(doc.createTextNode("Win32"))
pg = project.appendChild(doc.createElement("PropertyGroup"))
pg.setAttribute("Label", "Globals")
n = pg.appendChild(doc.createElement("ProjectName"))
n.appendChild(doc.createTextNode(name))
k = pg.appendChild(doc.createElement("Keyword"))
k.appendChild(doc.createTextNode("MakeFileProj"))
g = pg.appendChild(doc.createElement("ProjectGuid"))
g.appendChild(doc.createTextNode("{%s}" % project_id))
rn = pg.appendChild(doc.createElement("RootNamespace"))
rn.appendChild(doc.createTextNode("mozilla"))
pts = pg.appendChild(doc.createElement("PlatformToolset"))
pts.appendChild(
doc.createTextNode(
visual_studio_product_to_platform_toolset_version(version)
)
)
i = project.appendChild(doc.createElement("Import"))
i.setAttribute("Project", "$(VCTargetsPath)\\Microsoft.Cpp.Default.props")
ig = project.appendChild(doc.createElement("ImportGroup"))
ig.setAttribute("Label", "ExtensionTargets")
ig = project.appendChild(doc.createElement("ImportGroup"))
ig.setAttribute("Label", "ExtensionSettings")
ig = project.appendChild(doc.createElement("ImportGroup"))
ig.setAttribute("Label", "PropertySheets")
i = ig.appendChild(doc.createElement("Import"))
i.setAttribute("Project", "mozilla.props")
pg = project.appendChild(doc.createElement("PropertyGroup"))
pg.setAttribute("Label", "Configuration")
ct = pg.appendChild(doc.createElement("ConfigurationType"))
ct.appendChild(doc.createTextNode("Makefile"))
pg = project.appendChild(doc.createElement("PropertyGroup"))
pg.setAttribute("Condition", "'$(Configuration)|$(Platform)'=='Build|Win32'")
if build_command:
n = pg.appendChild(doc.createElement("NMakeBuildCommandLine"))
n.appendChild(doc.createTextNode(build_command))
if clean_command:
n = pg.appendChild(doc.createElement("NMakeCleanCommandLine"))
n.appendChild(doc.createTextNode(clean_command))
if includes:
n = pg.appendChild(doc.createElement("NMakeIncludeSearchPath"))
n.appendChild(doc.createTextNode(";".join(includes)))
if forced_includes:
n = pg.appendChild(doc.createElement("NMakeForcedIncludes"))
n.appendChild(doc.createTextNode(";".join(forced_includes)))
if defines:
n = pg.appendChild(doc.createElement("NMakePreprocessorDefinitions"))
n.appendChild(doc.createTextNode(";".join(defines)))
if debugger:
n = pg.appendChild(doc.createElement("LocalDebuggerCommand"))
n.appendChild(doc.createTextNode(debugger[0]))
n = pg.appendChild(doc.createElement("LocalDebuggerCommandArguments"))
n.appendChild(doc.createTextNode(debugger[1]))
# Sets IntelliSense to use c++17 Language Standard
n = pg.appendChild(doc.createElement("AdditionalOptions"))
n.appendChild(doc.createTextNode("/std:c++17"))
i = project.appendChild(doc.createElement("Import"))
i.setAttribute("Project", "$(VCTargetsPath)\\Microsoft.Cpp.props")
i = project.appendChild(doc.createElement("Import"))
i.setAttribute("Project", "$(VCTargetsPath)\\Microsoft.Cpp.targets")
# Now add files to the project.
ig = project.appendChild(doc.createElement("ItemGroup"))
for header in sorted(headers or []):
n = ig.appendChild(doc.createElement("ClInclude"))
n.setAttribute("Include", header)
ig = project.appendChild(doc.createElement("ItemGroup"))
for source in sorted(sources or []):
n = ig.appendChild(doc.createElement("ClCompile"))
n.setAttribute("Include", source)
fh.write(b"\xef\xbb\xbf")
doc.writexml(fh, addindent=" ", newl="\r\n")
return project_id, name