blob: 5799f9257c6093ebde72cc59bb31fe06efbbb3ea [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "dev-install/dev_install.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <iostream>
#include <istream>
#include <string>
#include <vector>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <brillo/key_value_store.h>
#include <brillo/process/process.h>
#include <vboot/crossystem.h>
namespace dev_install {
namespace {
// The root path that we install our dev packages into.
constexpr char kUsrLocal[] = "/usr/local";
// The Portage config path as a subdir under the various roots.
constexpr char kPortageConfigSubdir[] = "etc/portage";
// Where binpkgs are saved as a subdir under the various roots.
constexpr char kBinpkgSubdir[] = "portage/packages";
// File listing of packages we need for bootstrapping.
constexpr char kBootstrapListing[] =
"/usr/share/dev-install/bootstrap.packages";
// Directory of rootfs provided packages.
constexpr char kRootfsProvidedDir[] = "/usr/share/dev-install/rootfs.provided";
// Path to lsb-release file.
constexpr char kLsbReleasePath[] = "/etc/lsb-release";
// The devserer URL for this developer build.
constexpr char kLsbChromeosDevserver[] = "CHROMEOS_DEVSERVER";
// The current OS version.
constexpr char kLsbChromeosReleaseVersion[] = "CHROMEOS_RELEASE_VERSION";
// Setting for the board name.
constexpr char kLsbChromeosReleaseBoard[] = "CHROMEOS_RELEASE_BOARD";
// The base URL of the repository holding our portage prebuilt binpkgs.
constexpr char kDefaultBinhostPrefix[] =
"https://commondatastorage.googleapis.com/chromeos-dev-installer/board";
} // namespace
DevInstall::DevInstall()
: reinstall_(false),
uninstall_(false),
yes_(false),
only_bootstrap_(false),
state_dir_(kUsrLocal),
binhost_(""),
binhost_version_(""),
jobs_(0) {}
DevInstall::DevInstall(const std::string& binhost,
const std::string& binhost_version,
bool reinstall,
bool uninstall,
bool yes,
bool only_bootstrap,
uint32_t jobs)
: reinstall_(reinstall),
uninstall_(uninstall),
yes_(yes),
only_bootstrap_(only_bootstrap),
state_dir_(kUsrLocal),
binhost_(binhost),
binhost_version_(binhost_version),
jobs_(jobs) {}
bool DevInstall::IsDevMode() const {
int value = ::VbGetSystemPropertyInt("cros_debug");
return value == 1;
}
bool DevInstall::PromptUser(std::istream& input, const std::string& prompt) {
if (yes_)
return true;
std::cout << prompt << "? (y/N) " << std::flush;
std::string buffer;
if (std::getline(input, buffer)) {
return (buffer == "y" || buffer == "y\n");
}
return false;
}
// We have a custom DeletePath helper because we don't want to descend across
// mount points, and no base:: helper supports that.
bool DevInstall::DeletePath(const struct stat& base_stat,
const base::FilePath& dir) {
base::FileEnumerator iter(dir, true,
base::FileEnumerator::FILES |
base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS);
for (base::FilePath current = iter.Next(); !current.empty();
current = iter.Next()) {
const auto& info(iter.GetInfo());
if (info.IsDirectory()) {
// Abort if the dir is mounted.
if (base_stat.st_dev != info.stat().st_dev) {
LOG(ERROR) << "directory is mounted: " << current.value();
return false;
}
// Clear the contents of this directory.
if (!DeletePath(base_stat, current))
return false;
// Clear this directory itself.
if (rmdir(current.value().c_str())) {
PLOG(ERROR) << "deleting failed: " << current.value();
return false;
}
} else {
if (unlink(current.value().c_str())) {
PLOG(ERROR) << "deleting failed: " << current.value();
return false;
}
}
}
return true;
}
bool DevInstall::CreateMissingDirectory(const base::FilePath& dir) {
if (!base::PathExists(dir)) {
if (!base::CreateDirectory(dir) ||
!base::SetPosixFilePermissions(dir, 0755)) {
PLOG(ERROR) << "Creating " << dir.value() << " failed";
return false;
}
}
return true;
}
bool DevInstall::WriteFile(const base::FilePath& file,
const std::string& data) {
if (base::WriteFile(file, data.c_str(), data.size()) != data.size()) {
PLOG(ERROR) << "Could not write " << file.value();
return false;
}
return true;
}
bool DevInstall::ClearStateDir(const base::FilePath& dir) {
LOG(INFO) << "To clean up, we will run:\n rm -rf /usr/local/\n"
<< "Any content you have stored in there will be deleted.";
if (!PromptUser(std::cin, "Remove all installed packages now")) {
LOG(INFO) << "Operation cancelled.";
return false;
}
// Normally we'd use base::DeleteFile, but we don't want to traverse mounts.
struct stat base_stat;
if (stat(dir.value().c_str(), &base_stat)) {
if (errno == ENOENT)
return true;
PLOG(ERROR) << "Could not access " << dir.value();
return false;
}
bool success = DeletePath(base_stat, dir);
if (success)
LOG(INFO) << "Removed all installed packages.";
else
LOG(ERROR) << "Deleting " << dir.value() << " failed";
return success;
}
bool DevInstall::InitializeStateDir(const base::FilePath& dir) {
// Create this loop so uncompressed files in /usr/local/usr/ will be reachable
// through /usr/local/.
// Note: /usr/local is mount-binded onto the /mnt/stateful_partition/dev_mode
// during chromeos_startup during boot for machines in dev_mode.
const base::FilePath usr = dir.Append("usr");
if (!base::PathExists(usr)) {
// Create /usr/local/usr -> . symlink.
if (!base::CreateSymbolicLink(base::FilePath("."), usr)) {
PLOG(ERROR) << "Creating " << usr.value() << " failed";
return false;
}
}
// Setup a /tmp space for random scripts to rely on.
const base::FilePath tmp = dir.Append("tmp");
if (!base::PathExists(tmp)) {
// Can't use base::SetPosixFilePermissions as that blocks +t mode.
if (!base::CreateDirectory(tmp) || chmod(tmp.value().c_str(), 01777)) {
PLOG(ERROR) << "Creating " << tmp.value() << " failed";
return false;
}
}
const base::FilePath local = usr.Append("local");
if (!base::PathExists(local)) {
// Create /usr/local/usr/local -> . symlink.
if (!base::CreateSymbolicLink(base::FilePath("."), local)) {
PLOG(ERROR) << "Creating " << local.value() << " failed";
return false;
}
}
// Set up symlinks for etc/{group,passwd}, so that packages can look up users
// and groups correctly.
const base::FilePath etc = usr.Append("etc");
if (!CreateMissingDirectory(etc))
return false;
// Create /usr/local/etc/group -> /etc/group symlink.
const base::FilePath group = etc.Append("group");
if (!base::PathExists(group)) {
if (!base::CreateSymbolicLink(base::FilePath("/etc/group"), group)) {
PLOG(ERROR) << "Creating " << group.value() << " failed";
return false;
}
}
// Create /usr/local/etc/passwd -> /etc/passwd symlink.
const base::FilePath passwd = etc.Append("passwd");
if (!base::PathExists(passwd)) {
if (!base::CreateSymbolicLink(base::FilePath("/etc/passwd"), passwd)) {
PLOG(ERROR) << "Creating " << passwd.value() << " failed";
return false;
}
}
return true;
}
bool DevInstall::LoadRuntimeSettings(const base::FilePath& lsb_release) {
brillo::KeyValueStore store;
if (!store.Load(lsb_release)) {
PLOG(WARNING) << "Could not read " << kLsbReleasePath;
return true;
}
if (!store.GetString(kLsbChromeosDevserver, &devserver_url_))
devserver_url_.clear();
if (store.GetString(kLsbChromeosReleaseBoard, &board_)) {
size_t pos = board_.find("-signed-");
if (pos != std::string::npos)
board_.erase(pos);
} else {
board_.clear();
}
// If --binhost_version wasn't specified, calculate it.
if (binhost_version_.empty()) {
store.GetString(kLsbChromeosReleaseVersion, &binhost_version_);
}
return true;
}
void DevInstall::InitializeBinhost() {
if (!binhost_.empty())
return;
if (!devserver_url_.empty()) {
LOG(INFO) << "Devserver URL set to: " << devserver_url_;
if (PromptUser(std::cin, "Use it as the binhost")) {
binhost_ = devserver_url_ + "/static/pkgroot/" + board_ + "/packages";
return;
}
}
binhost_ = std::string(kDefaultBinhostPrefix) + "/" + board_ + "/" +
binhost_version_ + "/packages";
}
bool DevInstall::DownloadAndInstallBootstrapPackage(
const std::string& package) {
const std::string url(binhost_ + "/" + package + ".tbz2");
const base::FilePath binpkg_dir = state_dir_.Append(kBinpkgSubdir);
const base::FilePath pkg = binpkg_dir.Append(package + ".tbz2");
const base::FilePath pkgdir = pkg.DirName();
if (!CreateMissingDirectory(pkgdir))
return false;
LOG(INFO) << "Downloading " + url;
brillo::ProcessImpl curl;
curl.SetSearchPath(true);
curl.AddArg("curl");
curl.AddArg("--fail");
curl.AddStringOption("-o", pkg.value());
curl.AddArg(url);
if (curl.Run() != 0) {
LOG(ERROR) << "Could not download package";
return false;
}
LOG(INFO) << "Unpacking " + pkg.value();
brillo::ProcessImpl tar;
tar.SetSearchPath(true);
tar.AddArg("tar");
tar.AddStringOption("-C", state_dir_.value());
tar.AddArg("-xjkf");
tar.AddArg(pkg.value());
if (tar.Run() != 0) {
LOG(ERROR) << "Could not extract package";
return false;
}
return true;
}
bool DevInstall::DownloadAndInstallBootstrapPackages(
const base::FilePath& listing) {
std::string data;
if (!base::ReadFileToString(listing, &data)) {
PLOG(ERROR) << "Unable to read " << listing.value();
return false;
}
std::vector<std::string> lines = base::SplitString(
data, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (lines.empty()) {
LOG(ERROR) << "Bootstrap package set is empty!";
return false;
}
for (const std::string& line : lines) {
if (!DownloadAndInstallBootstrapPackage(line))
return false;
}
// The python ebuilds set up symlinks in pkg_postinst, but we don't run those
// phases (we just run untar above). Plus that logic depends on eselect that
// we currently stub out. Hand create the symlinks https://crbug.com/955147.
const base::FilePath python = state_dir_.Append("usr/bin/python");
if (!base::PathExists(python)) {
if (!base::CreateSymbolicLink(base::FilePath("python-wrapper"), python)) {
PLOG(ERROR) << "Creating " << python.value() << " failed";
return false;
}
}
const base::FilePath python2 = state_dir_.Append("usr/bin/python2");
if (!base::PathExists(python2)) {
if (!base::CreateSymbolicLink(base::FilePath("python2.7"), python2)) {
PLOG(ERROR) << "Creating " << python2.value() << " failed";
return false;
}
}
const base::FilePath python3 = state_dir_.Append("usr/bin/python3");
if (!base::PathExists(python3)) {
if (!base::CreateSymbolicLink(base::FilePath("python3.6"), python3)) {
PLOG(ERROR) << "Creating " << python3.value() << " failed";
return false;
}
}
return true;
}
bool DevInstall::ConfigurePortage() {
const base::FilePath portage_dir = state_dir_.Append(kPortageConfigSubdir);
const base::FilePath profile_dir = portage_dir.Append("make.profile");
// Copy emerge configuration to /usr/local.
if (!CreateMissingDirectory(profile_dir))
return false;
// Point our local profile to the rootfs one. This allows us to stack.
const base::FilePath parent_path = profile_dir.Append("parent");
const std::string parent_data{"/etc/portage/make.profile\n"};
if (!WriteFile(parent_path, parent_data))
return false;
// Install the package.provided entries for the rootfs.
const base::FilePath provided_path = profile_dir.Append("package.provided");
if (!CreateMissingDirectory(provided_path))
return false;
const base::FilePath rootfs_provided{kRootfsProvidedDir};
base::FileEnumerator profile_iter(rootfs_provided, false,
base::FileEnumerator::FILES);
for (base::FilePath current = profile_iter.Next(); !current.empty();
current = profile_iter.Next()) {
const base::FilePath sym = provided_path.Append(current.BaseName());
if (!base::CreateSymbolicLink(current, sym)) {
PLOG(ERROR) << "Creating " << sym.value() << " failed";
return false;
}
}
// Create the directories defined in the portage config files. Permissions are
// consistent with the other directories in /usr/local, which is a bind mount
// for /mnt/stateful_partition/dev_image.
//
// We set ROOT in make.conf as portage ignores it in profile make.defaults.
const base::FilePath make_conf_path = portage_dir.Append("make.conf");
const std::string make_conf_data{"ROOT=\"" + state_dir_.value() +
"\"\n"
"PORTAGE_BINHOST=\"" +
binhost_ + "\"\n"};
if (!WriteFile(make_conf_path, make_conf_data))
return false;
// Hack in LD_LIBRARY_PATH within portage env. Otherwise builds will filter
// it out which breaks python as it can't find its libs in /usr/local. This
// only shows up on base images as those won't have ldconfig run since the
// rootfs is read-only. See https://crbug.com/1065727 for examples.
//
// The path we search for will be:
// /usr/local/ (state_dir_)
// lib*/python*/site-packages/portage/ (internal python packages dir)
// package/ebuild/_config/special_env_vars.py
//
// We do this manually because base::FileEnumerator is unable to handle
// symlink loops which we have in /usr/local by design.
bool found_file = false;
base::FileEnumerator lib_iter(state_dir_, false,
base::FileEnumerator::DIRECTORIES, "lib*");
for (auto lib_current = lib_iter.Next(); !lib_current.empty();
lib_current = lib_iter.Next()) {
base::FileEnumerator python_iter(
lib_current, false, base::FileEnumerator::DIRECTORIES, "python3.*");
for (auto python_current = python_iter.Next(); !python_current.empty();
python_current = python_iter.Next()) {
const auto current = python_current.Append(
"site-packages/portage/package/ebuild/_config/special_env_vars.py");
if (!base::PathExists(current)) {
LOG(WARNING) << "Portage file does not exist: " << current.value();
continue;
}
found_file = true;
LOG(INFO) << "Hacking portage file for dev-install " << current.value();
// Load the file into memory.
std::string data;
if (!base::ReadFileToString(current, &data)) {
PLOG(ERROR) << "Unable to read " << current.value();
return false;
}
// Split it into lines.
auto lines = base::SplitStringPiece(data, "\n", base::KEEP_WHITESPACE,
base::SPLIT_WANT_ALL);
if (lines.empty()) {
LOG(ERROR) << "Portage internal file is empty: " << current.value();
return false;
}
// Replace the one line that we need.
const char kSearch[] = "environ_whitelist = []";
const char kReplace[] = "environ_whitelist = ['LD_LIBRARY_PATH']";
bool found_line = false;
for (auto& line : lines) {
if (line == kSearch) {
found_line = true;
line = kReplace;
}
}
if (!found_line) {
LOG(WARNING) << current.value() << ": Unable to find line to modify!";
}
// Write it back out.
data = base::JoinString(lines, "\n");
if (!base::WriteFile(current, data.data(), data.size())) {
LOG(ERROR) << "Unable to write " << current.value();
return false;
}
}
}
if (!found_file) {
LOG(WARNING) << "Unable to locate internal portage special_env_vars.py!";
LOG(WARNING) << "`emerge` might not work on your device; trying anyways.";
}
return true;
}
bool DevInstall::InstallExtraPackages() {
if (!PromptUser(std::cin, "Install virtual/target-os-dev package now")) {
LOG(INFO) << "You can install virtual/target-os-dev later by running:\n"
<< "emerge virtual/target-os-dev";
return true;
}
brillo::ProcessImpl emerge;
emerge.SetSearchPath(true);
emerge.AddArg("emerge");
int jobs = jobs_;
if (jobs == 0)
jobs = std::max(1L, sysconf(_SC_NPROCESSORS_ONLN) - 1);
emerge.AddArg("--jobs=" + std::to_string(jobs));
emerge.AddArg("virtual/target-os-dev");
if (emerge.Run() != 0) {
LOG(ERROR) << "Could not install virtual/target-os-dev";
return false;
}
return true;
}
int DevInstall::Run() {
// Only run if dev mode is enabled.
if (!IsDevMode()) {
LOG(ERROR) << "Chrome OS is not in developer mode";
return 2;
}
// Handle reinstall & uninstall operations.
if (reinstall_ || uninstall_) {
if (!ClearStateDir(state_dir_))
return 1;
if (uninstall_)
return 0;
LOG(INFO) << "Reinstalling dev state";
}
// See if the system has been initialized already.
const base::FilePath portage_dir = state_dir_.Append(kPortageConfigSubdir);
if (base::DirectoryExists(portage_dir)) {
LOG(ERROR) << "Directory " << portage_dir.value() << " exists.";
LOG(ERROR) << "Did you mean dev_install --reinstall?";
return 4;
}
// Initialize the base set of paths before we install any packages.
if (!InitializeStateDir(state_dir_))
return 5;
// Load the settings from the active device.
if (!LoadRuntimeSettings(base::FilePath(kLsbReleasePath)))
return 6;
// Parses flags to return the binhost or if none set, use the default binhost
// set up from installation.
InitializeBinhost();
LOG(INFO) << "Using binhost: " << binhost_;
// Bootstrap the setup.
LOG(INFO) << "Starting installation of developer packages.";
LOG(INFO) << "First, we download the necessary files.";
if (!DownloadAndInstallBootstrapPackages(base::FilePath(kBootstrapListing)))
return 7;
if (only_bootstrap_) {
LOG(INFO) << "Done installing bootstrap packages. Enjoy!";
return 0;
}
LOG(INFO) << "Developer packages initialized; configuring portage.";
if (!ConfigurePortage())
return 8;
LOG(INFO) << "Installing additional optional packages.";
if (!InstallExtraPackages())
return 9;
LOG(INFO) << "Done! Enjoy! Or not, you choose!";
return 0;
}
} // namespace dev_install