blob: c34172f32ff282b8e369883edaccc7bf9778d187 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
This is script to upload ninja_log from googler.
Server side implementation is in
https://cs.chromium.org/chromium/infra/go/src/infra/appengine/chromium_build_stats/
Uploaded ninjalog is stored in BigQuery table having following schema.
https://cs.chromium.org/chromium/infra/go/src/infra/appengine/chromium_build_stats/ninjaproto/ninjalog.proto
The log will be used to analyze user side build performance.
See also the privacy review. http://eldar/assessments/656778450
"""
import argparse
import gzip
import io
import json
import logging
import multiprocessing
import os
import platform
import subprocess
import sys
import time
import urllib.request
# These build configs affect build performance.
ALLOWLISTED_CONFIGS = (
"symbol_level",
"use_goma",
"use_remoteexec",
"use_siso",
"is_debug",
"is_component_build",
"enable_nacl",
"host_os",
"host_cpu",
"target_os",
"target_cpu",
"blink_symbol_level",
"is_java_debug",
"treat_warnings_as_errors",
"disable_android_lint",
"use_errorprone_java_compiler",
"incremental_install",
"android_static_analysis",
)
def IsGoogler():
"""Check whether this user is Googler or not."""
p = subprocess.run(
"cipd auth-info",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True,
)
if p.returncode != 0:
return False
lines = p.stdout.splitlines()
if len(lines) == 0:
return False
l = lines[0]
# |l| will be like 'Logged in as <user>@google.com.' for googler using
# reclient.
return l.startswith("Logged in as ") and l.endswith("@google.com.")
def ParseGNArgs(gn_args):
"""Parse gn_args as json and return config dictionary."""
configs = json.loads(gn_args)
build_configs = {}
for config in configs:
key = config["name"]
if key not in ALLOWLISTED_CONFIGS:
continue
if "current" in config:
build_configs[key] = config["current"]["value"]
else:
build_configs[key] = config["default"]["value"]
return build_configs
def GetBuildTargetFromCommandLine(cmdline):
"""Get build targets from commandline."""
# Skip argv0, argv1: ['/path/to/python3', '/path/to/depot_tools/ninja.py']
idx = 2
# Skipping all args that involve these flags, and taking all remaining args
# as targets.
onearg_flags = ("-C", "-d", "-f", "-j", "-k", "-l", "-p", "-t", "-w")
zeroarg_flags = ("--version", "-n", "-v")
targets = []
while idx < len(cmdline):
arg = cmdline[idx]
if arg in onearg_flags:
idx += 2
continue
if arg[:2] in onearg_flags or arg in zeroarg_flags:
idx += 1
continue
# A target doesn't start with '-'.
if arg.startswith("-"):
idx += 1
continue
# Avoid uploading absolute paths accidentally. e.g. b/270907050
if os.path.isabs(arg):
idx += 1
continue
targets.append(arg)
idx += 1
return targets
def GetJflag(cmdline):
"""Parse cmdline to get flag value for -j"""
for i in range(len(cmdline)):
if (cmdline[i] == "-j" and i + 1 < len(cmdline)
and cmdline[i + 1].isdigit()):
return int(cmdline[i + 1])
if cmdline[i].startswith("-j") and cmdline[i][len("-j"):].isdigit():
return int(cmdline[i][len("-j"):])
def GetMetadata(cmdline, ninjalog):
"""Get metadata for uploaded ninjalog.
Returned metadata has schema defined in
https://cs.chromium.org?q="type+Metadata+struct+%7B"+file:%5Einfra/go/src/infra/appengine/chromium_build_stats/ninjalog/
TODO(tikuta): Collect GOMA_* env var.
"""
build_dir = os.path.dirname(ninjalog)
build_configs = {}
try:
args = ["gn", "args", build_dir, "--list", "--short", "--json"]
if sys.platform == "win32":
# gn in PATH is bat file in windows environment (except cygwin).
args = ["cmd", "/c"] + args
gn_args = subprocess.check_output(args)
build_configs = ParseGNArgs(gn_args)
except subprocess.CalledProcessError as e:
logging.error("Failed to call gn %s", e)
build_configs = {}
# Stringify config.
for k in build_configs:
build_configs[k] = str(build_configs[k])
metadata = {
"platform": platform.system(),
"cpu_core": multiprocessing.cpu_count(),
"build_configs": build_configs,
"targets": GetBuildTargetFromCommandLine(cmdline),
}
jflag = GetJflag(cmdline)
if jflag is not None:
metadata["jobs"] = jflag
return metadata
def GetNinjalog(cmdline):
"""GetNinjalog returns the path to ninjalog from cmdline."""
# ninjalog is in current working directory by default.
ninjalog_dir = "."
i = 0
while i < len(cmdline):
cmd = cmdline[i]
i += 1
if cmd == "-C" and i < len(cmdline):
ninjalog_dir = cmdline[i]
i += 1
continue
if cmd.startswith("-C") and len(cmd) > len("-C"):
ninjalog_dir = cmd[len("-C"):]
return os.path.join(ninjalog_dir, ".ninja_log")
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--server",
default="chromium-build-stats.appspot.com",
help="server to upload ninjalog file.",
)
parser.add_argument("--ninjalog", help="ninjalog file to upload.")
parser.add_argument("--verbose",
action="store_true",
help="Enable verbose logging.")
parser.add_argument(
"--cmdline",
required=True,
nargs=argparse.REMAINDER,
help="command line args passed to ninja.",
)
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.INFO)
else:
# Disable logging.
logging.disable(logging.CRITICAL)
if not IsGoogler():
return 0
ninjalog = args.ninjalog or GetNinjalog(args.cmdline)
if not os.path.isfile(ninjalog):
logging.warning("ninjalog is not found in %s", ninjalog)
return 1
# We assume that each ninja invocation interval takes at least 2 seconds.
# This is not to have duplicate entry in server when current build is no-op.
if os.stat(ninjalog).st_mtime < time.time() - 2:
logging.info("ninjalog is not updated recently %s", ninjalog)
return 0
output = io.BytesIO()
with open(ninjalog) as f:
with gzip.GzipFile(fileobj=output, mode="wb") as g:
g.write(f.read().encode())
g.write(b"# end of ninja log\n")
metadata = GetMetadata(args.cmdline, ninjalog)
logging.info("send metadata: %s", json.dumps(metadata))
g.write(json.dumps(metadata).encode())
resp = urllib.request.urlopen(
urllib.request.Request(
"https://" + args.server + "/upload_ninja_log/",
data=output.getvalue(),
headers={"Content-Encoding": "gzip"},
))
if resp.status != 200:
logging.warning("unexpected status code for response: %s", resp.status)
return 1
logging.info("response header: %s", resp.headers)
logging.info("response content: %s", resp.read())
return 0
if __name__ == "__main__":
sys.exit(main())