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/.
import argparse
import logging
import os
import subprocess
import sys
import mozpack.path as mozpath
from mach.decorators import Command, CommandArgument
from mozfile import which
from mozbuild import build_commands
@Command(
"ide",
category="devenv",
description="Generate a project and launch an IDE.",
virtualenv_name="build",
)
@CommandArgument("ide", choices=["eclipse", "visualstudio", "vscode"])
@CommandArgument(
"--no-interactive",
default=False,
action="store_true",
help="Just generate the configuration",
)
@CommandArgument("args", nargs=argparse.REMAINDER)
def run(command_context, ide, no_interactive, args):
interactive = not no_interactive
if ide == "eclipse":
backend = "CppEclipse"
elif ide == "visualstudio":
backend = "VisualStudio"
elif ide == "vscode":
backend = "Clangd"
if ide == "eclipse" and not which("eclipse"):
command_context.log(
logging.ERROR,
"ide",
{},
"Eclipse CDT 8.4 or later must be installed in your PATH.",
)
command_context.log(
logging.ERROR,
"ide",
{},
)
return 1
if ide == "vscode":
rc = build_commands.configure(command_context)
if rc != 0:
return rc
# First install what we can through install manifests.
rc = command_context._run_make(
directory=command_context.topobjdir,
target="pre-export",
line_handler=None,
)
if rc != 0:
return rc
# Then build the rest of the build dependencies by running the full
# export target, because we can't do anything better.
for target in ("export", "pre-compile"):
rc = command_context._run_make(
directory=command_context.topobjdir,
target=target,
line_handler=None,
)
if rc != 0:
return rc
else:
# Here we refresh the whole build. 'build export' is sufficient here and is
# probably more correct but it's also nice having a single target to get a fully
# built and indexed project (gives a easy target to use before go out to lunch).
res = command_context._mach_context.commands.dispatch(
"build", command_context._mach_context
)
if res != 0:
return 1
# Generate or refresh the IDE backend.
python = command_context.virtualenv_manager.python_path
config_status = os.path.join(command_context.topobjdir, "config.status")
args = [python, config_status, "--backend=%s" % backend]
res = command_context._run_command_in_objdir(
args=args, pass_thru=True, ensure_exit_code=False
)
if res != 0:
return 1
if ide == "eclipse":
eclipse_workspace_dir = get_eclipse_workspace_path(command_context)
subprocess.check_call(["eclipse", "-data", eclipse_workspace_dir])
elif ide == "visualstudio":
visual_studio_workspace_dir = get_visualstudio_workspace_path(command_context)
subprocess.call(["explorer.exe", visual_studio_workspace_dir])
elif ide == "vscode":
return setup_vscode(command_context, interactive)
def get_eclipse_workspace_path(command_context):
from mozbuild.backend.cpp_eclipse import CppEclipseBackend
return CppEclipseBackend.get_workspace_path(
command_context.topsrcdir, command_context.topobjdir
)
def get_visualstudio_workspace_path(command_context):
return os.path.normpath(
os.path.join(command_context.topobjdir, "msvc", "mozilla.sln")
)
def setup_vscode(command_context, interactive):
from mozbuild.backend.clangd import find_vscode_cmd
# Check if platform has VSCode installed
if interactive:
vscode_cmd = find_vscode_cmd()
if vscode_cmd is None:
choice = prompt_bool(
"VSCode cannot be found, and may not be installed. Proceed?"
)
if not choice:
return 1
vscode_settings = mozpath.join(
command_context.topsrcdir, ".vscode", "settings.json"
)
new_settings = {}
artifact_prefix = ""
if command_context.config_environment.is_artifact_build:
artifact_prefix = (
"\nArtifact build configured: Skipping clang and rust setup. "
"If you later switch to a full build, please re-run this command."
)
else:
new_settings = setup_clangd_rust_in_vscode(command_context)
relobjdir = mozpath.relpath(command_context.topobjdir, command_context.topsrcdir)
# Add file associations.
new_settings = {
**new_settings,
"files.associations": {
"*.jsm": "javascript",
"*.sjs": "javascript",
},
# Note, the top-level editor settings are left as default to allow the
# user's defaults (if any) to take effect.
"[javascript][javascriptreact][typescript][typescriptreact][json][jsonc][html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": True,
},
"files.exclude": {"obj-*": True, relobjdir: True},
"files.watcherExclude": {"obj-*": True, relobjdir: True},
}
import difflib
import json
# Load the existing .vscode/settings.json file, to check if if needs to
# be created or updated.
try:
with open(vscode_settings) as fh:
old_settings_str = fh.read()
except FileNotFoundError:
print(
"Configuration for {} will be created.{}".format(
vscode_settings, artifact_prefix
)
)
old_settings_str = None
if old_settings_str is None:
# No old settings exist
with open(vscode_settings, "w") as fh:
json.dump(new_settings, fh, indent=4)
else:
# Merge our new settings with the existing settings, and check if we
# need to make changes. Only prompt & write out the updated config
# file if settings actually changed.
try:
old_settings = json.loads(old_settings_str)
prompt_prefix = ""
except ValueError:
old_settings = {}
prompt_prefix = (
"\n**WARNING**: Parsing of existing settings file failed. "
"Existing settings will be lost!"
)
# If we've got an old section with the formatting configuration, remove it
# so that we effectively "upgrade" the user to include json from the new
# settings. The user is presented with the diffs so should spot any issues.
deprecated = [
"[javascript][javascriptreact][typescript][typescriptreact]",
"[javascript][javascriptreact][typescript][typescriptreact][json]",
"[javascript][javascriptreact][typescript][typescriptreact][json][html]",
]
for entry in deprecated:
if entry in old_settings:
old_settings.pop(entry)
settings = {**old_settings, **new_settings}
if old_settings != settings:
# Prompt the user with a diff of the changes we're going to make
new_settings_str = json.dumps(settings, indent=4)
if interactive:
print(
"\nThe following modifications to {settings} will occur:\n{diff}".format(
settings=vscode_settings,
diff="".join(
difflib.unified_diff(
old_settings_str.splitlines(keepends=True),
new_settings_str.splitlines(keepends=True),
"a/.vscode/settings.json",
"b/.vscode/settings.json",
n=30,
)
),
)
)
choice = prompt_bool(
"{}{}\nProceed with modifications to {}?".format(
artifact_prefix, prompt_prefix, vscode_settings
)
)
if not choice:
return 1
with open(vscode_settings, "w") as fh:
fh.write(new_settings_str)
if not interactive:
return 0
# Open vscode with new configuration, or ask the user to do so if the
# binary was not found.
if vscode_cmd is None:
print(
"Please open VS Code manually and load directory: {}".format(
command_context.topsrcdir
)
)
return 0
rc = subprocess.call(vscode_cmd + [command_context.topsrcdir])
if rc != 0:
command_context.log(
logging.ERROR,
"ide",
{},
"Unable to open VS Code. Please open VS Code manually and load "
"directory: {}".format(command_context.topsrcdir),
)
return rc
return 0
def setup_clangd_rust_in_vscode(command_context):
clangd_cc_path = mozpath.join(command_context.topobjdir, "clangd")
# Verify if the required files are present
clang_tools_path = mozpath.join(
command_context._mach_context.state_dir, "clang-tools"
)
clang_tidy_bin = mozpath.join(clang_tools_path, "clang-tidy", "bin")
clangd_path = mozpath.join(
clang_tidy_bin,
"clangd" + command_context.config_environment.substs.get("BIN_SUFFIX", ""),
)
if not os.path.exists(clangd_path):
command_context.log(
logging.ERROR,
"ide",
{},
"Unable to locate clangd in {}.".format(clang_tidy_bin),
)
rc = get_clang_tools(command_context, clang_tools_path)
if rc != 0:
return rc
import multiprocessing
from mozbuild.code_analysis.utils import ClangTidyConfig
clang_tidy_cfg = ClangTidyConfig(command_context.topsrcdir)
if sys.platform == "win32":
cargo_check_command = [sys.executable, "mach"]
else:
cargo_check_command = ["./mach"]
cargo_check_command += [
"--log-no-times",
"cargo",
"check",
"-j",
str(multiprocessing.cpu_count() // 2),
"--all-crates",
"--message-format-json",
]
clang_tidy = {}
clang_tidy["Checks"] = ",".join(clang_tidy_cfg.checks)
clang_tidy.update(clang_tidy_cfg.checks_config)
# Write .clang-tidy yml
import yaml
with open(".clang-tidy", "w") as file:
yaml.dump(clang_tidy, file)
clangd_cfg = {
"CompileFlags": {
"CompilationDatabase": clangd_cc_path,
}
}
with open(".clangd", "w") as file:
yaml.dump(clangd_cfg, file)
return {
"clangd.path": clangd_path,
"clangd.arguments": [
"-j",
str(multiprocessing.cpu_count() // 2),
"--limit-results",
"0",
"--completion-style",
"detailed",
"--background-index",
"--all-scopes-completion",
"--log",
"info",
"--pch-storage",
"disk",
"--clang-tidy",
"--header-insertion=never",
],
"rust-analyzer.server.extraEnv": {
# Point rust-analyzer at the real target directory used by our
# build, so it can discover the files created when we run `./mach
# cargo check`.
"CARGO_TARGET_DIR": command_context.topobjdir,
},
"rust-analyzer.cargo.buildScripts.overrideCommand": cargo_check_command,
"rust-analyzer.check.overrideCommand": cargo_check_command,
}
def get_clang_tools(command_context, clang_tools_path):
import shutil
if os.path.isdir(clang_tools_path):
shutil.rmtree(clang_tools_path)
# Create base directory where we store clang binary
os.mkdir(clang_tools_path)
from mozbuild.artifact_commands import artifact_toolchain
job, _ = command_context.platform
if job is None:
command_context.log(
logging.ERROR,
"ide",
{},
"The current platform isn't supported. "
"Currently only the following platforms are "
"supported: win32/win64, linux64 and macosx64.",
)
return 1
job += "-clang-tidy"
# We want to unpack data in the clang-tidy mozbuild folder
currentWorkingDir = os.getcwd()
os.chdir(clang_tools_path)
rc = artifact_toolchain(
command_context, verbose=False, from_build=[job], no_unpack=False, retry=0
)
# Change back the cwd
os.chdir(currentWorkingDir)
return rc
def prompt_bool(prompt, limit=5):
"""Prompts the user with prompt and requires a boolean value."""
from mach.util import strtobool
for _ in range(limit):
try:
return strtobool(input(prompt + " [Y/N]\n"))
except ValueError:
print(
"ERROR! Please enter a valid option! Please use any of the following:"
" Y, N, True, False, 1, 0"
)
return False