blob: 8c3004f8d0c21baeb504f71190dbf7731de3c841 [file] [log] [blame]
# 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.
from __future__ import absolute_import, unicode_literals
import errno
import os
import re
import types
import uuid
from xml.dom import getDOMImplementation
from mozpack.files import FileFinder
from .common import CommonBackend
from ..frontend.data import (
Defines,
GeneratedSources,
HostSources,
Library,
LocalInclude,
Sources,
UnifiedSources,
)
from mozbuild.base import ExecutionSummary
MSBUILD_NAMESPACE = 'http://schemas.microsoft.com/developer/msbuild/2003'
def get_id(name):
return str(uuid.uuid5(uuid.NAMESPACE_URL, name)).upper()
# TODO validate mappings are correct. only 2010 confirmed so far
def visual_studio_product_to_internal_version(version, solution=False):
if solution:
if version == '2010':
return '11.00'
elif version == '2011':
return '12.00'
elif version == '2012':
return '12.00'
elif version == '2013':
return '12.00'
else:
raise Exception('Unknown version seen: %s' % version)
else:
if version == '2010':
return '10.00'
elif version == '2011':
return '11.00'
elif version == '2012':
return '12.00'
elif version == '2013':
return '12.00'
else:
raise Exception('Unknown version seen: %s' % version)
def visual_studio_product_to_platform_toolset_version(version):
if version == '2010':
return 'v100'
elif version == '2011':
return 'v110'
elif version == '2012':
return 'v120'
elif version == '2013':
return 'v120'
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.
It's worth noting that lots of file I/O here is not using
self._write_file(). That's because we need Windows line endings preserved
and self._write_file() currently opens files in text mode, which behaves
oddly under MozillaBuild.
"""
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'
# But making this one a parameter requires testing first.
self._version = '2013'
self._paths_to_sources = {}
self._paths_to_includes = {}
self._paths_to_defines = {}
self._paths_to_configs = {}
self._libs_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, 'relativedir', 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, GeneratedSources):
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):
self._libs_to_paths[obj.basename] = 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, 'relativedir', 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)
try:
os.makedirs(out_dir)
except OSError as e:
if e.errno != errno.EEXIST:
raise
try:
os.makedirs(out_proj_dir)
except OSError as e:
if e.errno != errno.EEXIST:
raise
projects = {}
for lib, path in sorted(self._libs_to_paths.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),
find_executables=False)
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))
basename = 'library_%s' % lib
project_id = self._write_vs_project(out_proj_dir, basename, lib,
includes=includes,
forced_includes=['$(TopObjDir)\\dist\\include\\mozilla-config.h'],
defines=defines,
headers=headers,
sources=sources)
projects[basename] = (project_id, basename, lib)
# 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 build clean')
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')
# A project to run the main application binary.
app_name = self.environment.substs['MOZ_APP_NAME']
basename = 'binary_%s' % app_name
project_id = self._write_vs_project(out_proj_dir, basename, app_name,
debugger=('$(TopObjDir)\\dist\\bin\\%s.exe' % app_name,
'-no-remote'))
projects[basename] = (project_id, basename, app_name)
# Projects to run other common binaries.
for app in ['js', 'xpcshell']:
basename = 'binary_%s' % app
project_id = self._write_vs_project(out_proj_dir, basename, app,
debugger=('$(TopObjDir)\\dist\\bin\\%s.exe' % app, ''))
projects[basename] = (project_id, basename, app)
# Write out a shared property file with common variables.
props_path = os.path.join(out_proj_dir, 'mozilla.props')
with open(props_path, 'wb') 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 open(os.path.join(out_dir, 'mach.bat'), 'wb') as fh:
self._write_mach_batch(fh)
with open(os.path.join(out_dir, 'mach.ps1'), 'wb') 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 open(solution_path, 'wb') as fh:
self._write_solution(fh, projects)
def _write_solution(self, fh, projects):
version = visual_studio_product_to_internal_version(self._version, True)
# 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' %
version)
fh.write('# Visual Studio %s\r\n' % self._version)
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.encode('utf-8'))
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))
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 _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, v))
relpath = os.path.relpath(self.environment.topsrcdir,
self.environment.topobjdir).replace('\\', '/')
fh.write(b'$bashargs = "%s/mach", "--log-no-times"\r\n' % relpath)
fh.write(b'$bashargs = $bashargs + $args\r\n')
fh.write(b"$expanded = $bashargs -join ' '\r\n")
fh.write(b'$procargs = "-c", $expanded\r\n')
fh.write(b'Start-Process -WorkingDirectory $env:TOPOBJDIR '
b'-FilePath $env:MOZILLABUILD\\msys\\bin\\bash '
b'-ArgumentList $procargs '
b'-Wait -NoNewWindow\r\n')
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, v))
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('\\', '/')
# We go through mach because it has the logic for choosing the most
# appropriate build tool.
fh.write(b'"%%MOZILLABUILD%%\\msys\\bin\\bash" '
b'-c "%s/mach --log-no-times %%1 %%2 %%3 %%4 %%5 %%6 %%7"' % relpath)
def _write_vs_project(self, out_dir, basename, name, **kwargs):
root = '%s.vcxproj' % basename
project_id = get_id(basename.encode('utf-8'))
with open(os.path.join(out_dir, root), 'wb') as fh:
project_id, name = VisualStudioBackend.write_vs_project(fh,
self._version, project_id, name, **kwargs)
with open(os.path.join(out_dir, '%s.user' % root), 'w') 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]))
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