bug 1483941 - prompt to enable build system telemetry in bootstrap. r=nalexander

This change adds a prompt to enable build system telemetry as part of
bootstrap. The prompt will only be shown if the build.telemetry config value
is not present, so users will not be prompted again if they have already
opted-in. However, if a user answers 'no' we don't save that value to
the config file because the default is to not send telemetry, so unless
they manually add `telemetry = false` to their config file they will be
prompted again the next time they run bootstrap.

The config value is always written to `~/.mozbuild/machrc` where we store
other Firefox build-related state. A standalone function is used to write
the config file so that we can do so even when running from bootstrap.py
outside of the context of a mach command.

As part of this change a `prompt_yesno` method is added to `BaseBootstrapper`.

Differential Revision: https://phabricator.services.mozilla.com/D9781

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ted Mielczarek 2018-11-06 14:19:24 +00:00
parent 405251a9f4
commit 8a92d6f72c
6 changed files with 188 additions and 4 deletions

View File

@ -55,6 +55,7 @@ with Files('mach/docs/**'):
PYTHON_UNITTEST_MANIFESTS += [
'mach/mach/test/python.ini',
'mozboot/mozboot/test/python.ini',
'mozbuild/dumbmake/test/python.ini',
'mozlint/test/python.ini',
'mozrelease/test/python.ini',

View File

@ -408,6 +408,20 @@ class BaseBootstrapper(object):
else:
raise Exception("Error! Reached max attempts of entering option.")
def prompt_yesno(self, prompt):
''' Prompts the user with prompt and requires a yes/no answer.'''
valid = False
while not valid:
choice = raw_input(prompt + ' [Y/n]: ').strip().lower()[:1]
if choice == '':
choice = 'y'
if choice not in ('y', 'n'):
print('ERROR! Please enter y or n!')
else:
valid = True
return choice == 'y'
def _ensure_package_manager_updated(self):
if self.package_manager_updated:
return

View File

@ -9,6 +9,16 @@ import platform
import sys
import os
import subprocess
try:
from ConfigParser import (
Error as ConfigParserError,
RawConfigParser,
)
except ImportError:
from configparser import (
Error as ConfigParserError,
RawConfigParser,
)
# Don't forgot to add new mozboot modules to the bootstrap download
# list in bin/bootstrap.py!
@ -185,17 +195,54 @@ lines:
Then restart your shell.
'''
TELEMETRY_OPT_IN_PROMPT = '''
Would you like to enable build system telemetry?
Mozilla collects data about local builds in order to make builds faster and
improve developer tooling. To learn more about the data we intend to collect
read here:
https://firefox-source-docs.mozilla.org/build/buildsystem/telemetry.html.
If you have questions, please ask in #build in irc.mozilla.org. If you would
like to opt out of data collection, select (N) at the prompt.
Your choice'''
def update_or_create_build_telemetry_config(path):
"""Write a mach config file enabling build telemetry to `path`. If the file does not exist,
create it. If it exists, add the new setting to the existing data.
This is standalone from mach's `ConfigSettings` so we can use it during bootstrap
without a source checkout.
"""
config = RawConfigParser()
if os.path.exists(path):
try:
config.read([path])
except ConfigParserError as e:
print('Your mach configuration file at `{path}` is not parseable:\n{error}'.format(
path=path, error=e))
return False
if not config.has_section('build'):
config.add_section('build')
config.set('build', 'telemetry', 'true')
with open(path, 'wb') as f:
config.write(f)
return True
class Bootstrapper(object):
"""Main class that performs system bootstrap."""
def __init__(self, finished=FINISHED, choice=None, no_interactive=False,
hg_configure=False, no_system_changes=False):
hg_configure=False, no_system_changes=False, mach_context=None):
self.instance = None
self.finished = finished
self.choice = choice
self.hg_configure = hg_configure
self.no_system_changes = no_system_changes
self.mach_context = mach_context
cls = None
args = {'no_interactive': no_interactive,
'no_system_changes': no_system_changes}
@ -336,6 +383,20 @@ class Bootstrapper(object):
self.instance.ensure_stylo_packages(state_dir, checkout_root)
self.instance.ensure_node_packages(state_dir, checkout_root)
def check_telemetry_opt_in(self, state_dir):
# We can't prompt the user.
if self.instance.no_interactive:
return
# Don't prompt if the user already has a setting for this value.
if self.mach_context is not None and 'telemetry' in self.mach_context.settings.build:
return
choice = self.instance.prompt_yesno(prompt=TELEMETRY_OPT_IN_PROMPT)
if choice:
cfg_file = os.path.join(state_dir, 'machrc')
if update_or_create_build_telemetry_config(cfg_file):
print('\nThanks for enabling build telemetry! You can change this setting at ' +
'any time by editing the config file `{}`\n'.format(cfg_file))
def bootstrap(self):
if self.choice is None:
# Like ['1. Firefox for Desktop', '2. Firefox for Android Artifact Mode', ...].
@ -360,6 +421,8 @@ class Bootstrapper(object):
(checkout_type, checkout_root) = r
have_clone = bool(checkout_type)
if state_dir_available:
self.check_telemetry_opt_in(state_dir)
self.maybe_install_private_packages_or_exit(state_dir,
state_dir_available,
have_clone,
@ -435,6 +498,8 @@ class Bootstrapper(object):
if not have_clone:
print(SOURCE_ADVERTISE)
if state_dir_available:
self.check_telemetry_opt_in(state_dir)
self.maybe_install_private_packages_or_exit(state_dir,
state_dir_available,
have_clone,

View File

@ -16,6 +16,8 @@ from mach.decorators import (
@CommandProvider
class Bootstrap(object):
"""Bootstrap system and mach for optimal development experience."""
def __init__(self, context):
self._context = context
@Command('bootstrap', category='devenv',
description='Install required system packages for building.')
@ -32,9 +34,12 @@ class Bootstrap(object):
def bootstrap(self, application_choice=None, no_interactive=False, no_system_changes=False):
from mozboot.bootstrap import Bootstrapper
bootstrapper = Bootstrapper(choice=application_choice,
no_interactive=no_interactive,
no_system_changes=no_system_changes)
bootstrapper = Bootstrapper(
choice=application_choice,
no_interactive=no_interactive,
no_system_changes=no_system_changes,
mach_context=self._context,
)
bootstrapper.bootstrap()

View File

@ -0,0 +1,4 @@
[DEFAULT]
skip-if = python == 3
[test_write_config.py]

View File

@ -0,0 +1,95 @@
# 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/.
from __future__ import absolute_import
import mozunit
import pytest
from mach.config import ConfigSettings
from mach.decorators import SettingsProvider
from mozboot.bootstrap import update_or_create_build_telemetry_config
# Duplicated from python/mozbuild/mozbuild/mach_commands.py because we can't
# actually import that module here.
@SettingsProvider
class TelemetrySettings():
config_settings = [
('build.telemetry', 'boolean', """
Enable submission of build system telemetry.
""".strip(), False),
]
@SettingsProvider
class OtherSettings():
config_settings = [
('foo.bar', 'int', '', 1),
('build.abc', 'string', '', ''),
]
def read(path):
s = ConfigSettings()
s.register_provider(TelemetrySettings)
s.register_provider(OtherSettings)
s.load_file(path)
return s
@pytest.fixture
def config_path(tmpdir):
return unicode(tmpdir.join('machrc'))
@pytest.fixture
def write_config(config_path):
def _config(contents):
with open(config_path, 'wb') as f:
f.write(contents)
return _config
def test_nonexistent(config_path):
update_or_create_build_telemetry_config(config_path)
s = read(config_path)
assert(s.build.telemetry)
def test_file_exists_no_build_section(config_path, write_config):
write_config('''[foo]
bar = 2
''')
update_or_create_build_telemetry_config(config_path)
s = read(config_path)
assert(s.build.telemetry)
assert(s.foo.bar == 2)
def test_existing_build_section(config_path, write_config):
write_config('''[foo]
bar = 2
[build]
abc = xyz
''')
update_or_create_build_telemetry_config(config_path)
s = read(config_path)
assert(s.build.telemetry)
assert(s.build.abc == 'xyz')
assert(s.foo.bar == 2)
def test_malformed_file(config_path, write_config):
"""Ensure that a malformed config file doesn't cause breakage."""
write_config('''[foo
bar = 1
''')
assert(not update_or_create_build_telemetry_config(config_path))
# Can't read config, it will not have been written!
if __name__ == '__main__':
mozunit.main()