diff options
Diffstat (limited to 'tools/bit')
-rw-r--r-- | tools/bit/Android.mk | 43 | ||||
-rw-r--r-- | tools/bit/aapt.cpp | 270 | ||||
-rw-r--r-- | tools/bit/aapt.h | 39 | ||||
-rw-r--r-- | tools/bit/adb.cpp | 463 | ||||
-rw-r--r-- | tools/bit/adb.h | 47 | ||||
-rw-r--r-- | tools/bit/command.cpp | 183 | ||||
-rw-r--r-- | tools/bit/command.h | 58 | ||||
-rw-r--r-- | tools/bit/main.cpp | 981 | ||||
-rw-r--r-- | tools/bit/make.cpp | 210 | ||||
-rw-r--r-- | tools/bit/make.h | 50 | ||||
-rw-r--r-- | tools/bit/print.cpp | 155 | ||||
-rw-r--r-- | tools/bit/print.h | 39 | ||||
-rw-r--r-- | tools/bit/util.cpp | 254 | ||||
-rw-r--r-- | tools/bit/util.h | 83 |
14 files changed, 2875 insertions, 0 deletions
diff --git a/tools/bit/Android.mk b/tools/bit/Android.mk new file mode 100644 index 000000000000..7f691a07b06a --- /dev/null +++ b/tools/bit/Android.mk @@ -0,0 +1,43 @@ +# +# Copyright (C) 2015 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) + +# ========================================================== +# Build the host executable: protoc-gen-javastream +# ========================================================== +include $(CLEAR_VARS) + +LOCAL_MODULE := bit + +LOCAL_SRC_FILES := \ + aapt.cpp \ + adb.cpp \ + command.cpp \ + main.cpp \ + make.cpp \ + print.cpp \ + util.cpp + +LOCAL_STATIC_LIBRARIES := \ + libexpat \ + libinstrumentation \ + libjsoncpp + +LOCAL_SHARED_LIBRARIES := \ + libprotobuf-cpp-full + +include $(BUILD_HOST_EXECUTABLE) + diff --git a/tools/bit/aapt.cpp b/tools/bit/aapt.cpp new file mode 100644 index 000000000000..961b47cdfecd --- /dev/null +++ b/tools/bit/aapt.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aapt.h" + +#include "command.h" +#include "print.h" +#include "util.h" + +#include <regex> + +const regex NS_REGEX("( *)N: ([^=]+)=(.*)"); +const regex ELEMENT_REGEX("( *)E: ([^ ]+) \\(line=(\\d+)\\)"); +const regex ATTR_REGEX("( *)A: ([^\\(=]+)[^=]*=\"([^\"]+)\".*"); + +const string ANDROID_NS("http://schemas.android.com/apk/res/android"); + +bool +Apk::HasActivity(const string& className) +{ + string fullClassName = full_class_name(package, className); + const size_t N = activities.size(); + for (size_t i=0; i<N; i++) { + if (activities[i] == fullClassName) { + return true; + } + } + return false; +} + +struct Attribute { + string ns; + string name; + string value; +}; + +struct Element { + Element* parent; + string ns; + string name; + int lineno; + vector<Attribute> attributes; + vector<Element*> children; + + /** + * Indentation in the xmltree dump. Might not be equal to the distance + * from the root because namespace rows (scopes) have their own indentation. + */ + int depth; + + Element(); + ~Element(); + + string GetAttr(const string& ns, const string& name) const; + void FindElements(const string& ns, const string& name, vector<Element*>* result, bool recurse); + +}; + +Element::Element() +{ +} + +Element::~Element() +{ + const size_t N = children.size(); + for (size_t i=0; i<N; i++) { + delete children[i]; + } +} + +string +Element::GetAttr(const string& ns, const string& name) const +{ + const size_t N = attributes.size(); + for (size_t i=0; i<N; i++) { + const Attribute& attr = attributes[i]; + if (attr.ns == ns && attr.name == name) { + return attr.value; + } + } + return string(); +} + +void +Element::FindElements(const string& ns, const string& name, vector<Element*>* result, bool recurse) +{ + const size_t N = children.size(); + for (size_t i=0; i<N; i++) { + Element* child = children[i]; + if (child->ns == ns && child->name == name) { + result->push_back(child); + } + if (recurse) { + child->FindElements(ns, name, result, recurse); + } + } +} + +struct Scope { + Scope* parent; + int depth; + map<string,string> namespaces; + + Scope(Scope* parent, int depth); +}; + +Scope::Scope(Scope* p, int d) + :parent(p), + depth(d) +{ + if (p != NULL) { + namespaces = p->namespaces; + } +} + + +string +full_class_name(const string& packageName, const string& className) +{ + if (className.length() == 0) { + return ""; + } + if (className[0] == '.') { + return packageName + className; + } + if (className.find('.') == string::npos) { + return packageName + "." + className; + } + return className; +} + +string +pretty_component_name(const string& packageName, const string& className) +{ + if (starts_with(packageName, className)) { + size_t pn = packageName.length(); + size_t cn = className.length(); + if (cn > pn && className[pn] == '.') { + return packageName + "/" + string(className, pn, string::npos); + } + } + return packageName + "/" + className; +} + +int +inspect_apk(Apk* apk, const string& filename) +{ + // Load the manifest xml + Command cmd("aapt"); + cmd.AddArg("dump"); + cmd.AddArg("xmltree"); + cmd.AddArg(filename); + cmd.AddArg("AndroidManifest.xml"); + + int err; + + string output = get_command_output(cmd, &err, false); + check_error(err); + + // Parse the manifest xml + Scope* scope = new Scope(NULL, -1); + Element* root = NULL; + Element* current = NULL; + vector<string> lines; + split_lines(&lines, output); + for (size_t i=0; i<lines.size(); i++) { + const string& line = lines[i]; + smatch match; + if (regex_match(line, match, NS_REGEX)) { + int depth = match[1].length() / 2; + while (depth < scope->depth) { + Scope* tmp = scope; + scope = scope->parent; + delete tmp; + } + scope = new Scope(scope, depth); + scope->namespaces[match[2]] = match[3]; + } else if (regex_match(line, match, ELEMENT_REGEX)) { + Element* element = new Element(); + + string str = match[2]; + size_t colon = str.find(':'); + if (colon == string::npos) { + element->name = str; + } else { + element->ns = scope->namespaces[string(str, 0, colon)]; + element->name.assign(str, colon+1, string::npos); + } + element->lineno = atoi(match[3].str().c_str()); + element->depth = match[1].length() / 2; + + if (root == NULL) { + current = element; + root = element; + } else { + while (element->depth <= current->depth && current->parent != NULL) { + current = current->parent; + } + element->parent = current; + current->children.push_back(element); + current = element; + } + } else if (regex_match(line, match, ATTR_REGEX)) { + if (current != NULL) { + Attribute attr; + string str = match[2]; + size_t colon = str.find(':'); + if (colon == string::npos) { + attr.name = str; + } else { + attr.ns = scope->namespaces[string(str, 0, colon)]; + attr.name.assign(str, colon+1, string::npos); + } + attr.value = match[3]; + current->attributes.push_back(attr); + } + } + } + while (scope != NULL) { + Scope* tmp = scope; + scope = scope->parent; + delete tmp; + } + + // Package name + apk->package = root->GetAttr("", "package"); + if (apk->package.size() == 0) { + print_error("%s:%d: Manifest root element doesn't contain a package attribute", + filename.c_str(), root->lineno); + delete root; + return 1; + } + + // Instrumentation runner + vector<Element*> instrumentation; + root->FindElements("", "instrumentation", &instrumentation, true); + if (instrumentation.size() > 0) { + // TODO: How could we deal with multiple instrumentation tags? + // We'll just pick the first one. + apk->runner = instrumentation[0]->GetAttr(ANDROID_NS, "name"); + } + + // Activities + vector<Element*> activities; + root->FindElements("", "activity", &activities, true); + for (size_t i=0; i<activities.size(); i++) { + string name = activities[i]->GetAttr(ANDROID_NS, "name"); + if (name.size() == 0) { + continue; + } + apk->activities.push_back(full_class_name(apk->package, name)); + } + + delete root; + return 0; +} + diff --git a/tools/bit/aapt.h b/tools/bit/aapt.h new file mode 100644 index 000000000000..6aeb03f18744 --- /dev/null +++ b/tools/bit/aapt.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_H +#define AAPT_H + +#include <string> +#include <vector> + +using namespace std; + +struct Apk +{ + string package; + string runner; + vector<string> activities; + + bool HasActivity(const string& className); +}; + +string full_class_name(const string& packageName, const string& className); +string pretty_component_name(const string& packageName, const string& className); + +int inspect_apk(Apk* apk, const string& filename); + +#endif // AAPT_H diff --git a/tools/bit/adb.cpp b/tools/bit/adb.cpp new file mode 100644 index 000000000000..eb96dae2189c --- /dev/null +++ b/tools/bit/adb.cpp @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "adb.h" + +#include "command.h" +#include "print.h" +#include "util.h" + +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <limits.h> + +#include <iostream> +#include <istream> +#include <streambuf> + +using namespace std; + +struct Buffer: public streambuf +{ + Buffer(char* begin, size_t size); +}; + +Buffer::Buffer(char* begin, size_t size) +{ + this->setg(begin, begin, begin + size); +} + +int +run_adb(const char* first, ...) +{ + Command cmd("adb"); + + if (first == NULL) { + return 0; + } + + cmd.AddArg(first); + + va_list args; + va_start(args, first); + while (true) { + const char* arg = va_arg(args, char*); + if (arg == NULL) { + break; + } + cmd.AddArg(arg); + } + va_end(args); + + return run_command(cmd); +} + +string +get_system_property(const string& name, int* err) +{ + Command cmd("adb"); + cmd.AddArg("shell"); + cmd.AddArg("getprop"); + cmd.AddArg(name); + + return trim(get_command_output(cmd, err, false)); +} + + +static uint64_t +read_varint(int fd, int* err, bool* done) +{ + uint32_t bits = 0; + uint64_t result = 0; + while (true) { + uint8_t byte; + ssize_t amt = read(fd, &byte, 1); + if (amt == 0) { + *done = true; + return result; + } else if (amt < 0) { + return *err = errno; + } + result |= uint64_t(byte & 0x7F) << bits; + if ((byte & 0x80) == 0) { + return result; + } + bits += 7; + if (bits > 64) { + *err = -1; + return 0; + } + } +} + +static char* +read_sized_buffer(int fd, int* err, size_t* resultSize) +{ + bool done = false; + uint64_t size = read_varint(fd, err, &done); + if (*err != 0 || done) { + return NULL; + } + if (size == 0) { + *resultSize = 0; + return NULL; + } + // 10 MB seems like a reasonable limit. + if (size > 10*1024*1024) { + print_error("result buffer too large: %llu", size); + return NULL; + } + char* buf = (char*)malloc(size); + if (buf == NULL) { + print_error("Can't allocate a buffer of size for test results: %llu", size); + return NULL; + } + int pos = 0; + while (size - pos > 0) { + ssize_t amt = read(fd, buf+pos, size-pos); + if (amt == 0) { + // early end of pipe + print_error("Early end of pipe."); + *err = -1; + free(buf); + return NULL; + } else if (amt < 0) { + // error + *err = errno; + free(buf); + return NULL; + } + pos += amt; + } + *resultSize = (size_t)size; + return buf; +} + +static int +read_sized_proto(int fd, Message* message) +{ + int err = 0; + size_t size; + char* buf = read_sized_buffer(fd, &err, &size); + if (err != 0) { + if (buf != NULL) { + free(buf); + } + return err; + } else if (size == 0) { + if (buf != NULL) { + free(buf); + } + return 0; + } else if (buf == NULL) { + return -1; + } + Buffer buffer(buf, size); + istream in(&buffer); + + err = message->ParseFromIstream(&in) ? 0 : -1; + + free(buf); + return err; +} + +static int +skip_bytes(int fd, ssize_t size, char* scratch, int scratchSize) +{ + while (size > 0) { + ssize_t amt = size < scratchSize ? size : scratchSize; + fprintf(stderr, "skipping %lu/%ld bytes\n", size, amt); + amt = read(fd, scratch, amt); + if (amt == 0) { + // early end of pipe + print_error("Early end of pipe."); + return -1; + } else if (amt < 0) { + // error + return errno; + } + size -= amt; + } + return 0; +} + +static int +skip_unknown_field(int fd, uint64_t tag, char* scratch, int scratchSize) { + bool done; + int err; + uint64_t size; + switch (tag & 0x7) { + case 0: // varint + read_varint(fd, &err, &done); + if (err != 0) { + return err; + } else if (done) { + return -1; + } else { + return 0; + } + case 1: + return skip_bytes(fd, 8, scratch, scratchSize); + case 2: + size = read_varint(fd, &err, &done); + if (err != 0) { + return err; + } else if (done) { + return -1; + } + if (size > INT_MAX) { + // we'll be here a long time but this keeps it from overflowing + return -1; + } + return skip_bytes(fd, (ssize_t)size, scratch, scratchSize); + case 5: + return skip_bytes(fd, 4, scratch, scratchSize); + default: + print_error("bad wire type for tag 0x%lx\n", tag); + return -1; + } +} + +static int +read_instrumentation_results(int fd, char* scratch, int scratchSize, + InstrumentationCallbacks* callbacks) +{ + bool done = false; + int err = 0; + string result; + while (true) { + uint64_t tag = read_varint(fd, &err, &done); + if (done) { + // Done reading input (this is the only place that a stream end isn't an error). + return 0; + } else if (err != 0) { + return err; + } else if (tag == 0xa) { // test_status + TestStatus status; + err = read_sized_proto(fd, &status); + if (err != 0) { + return err; + } + callbacks->OnTestStatus(status); + } else if (tag == 0x12) { // session_status + SessionStatus status; + err = read_sized_proto(fd, &status); + if (err != 0) { + return err; + } + callbacks->OnSessionStatus(status); + } else { + err = skip_unknown_field(fd, tag, scratch, scratchSize); + if (err != 0) { + return err; + } + } + } + return 0; +} + +int +run_instrumentation_test(const string& packageName, const string& runner, const string& className, + InstrumentationCallbacks* callbacks) +{ + Command cmd("adb"); + cmd.AddArg("shell"); + cmd.AddArg("am"); + cmd.AddArg("instrument"); + cmd.AddArg("-w"); + cmd.AddArg("-m"); + if (className.length() > 0) { + cmd.AddArg("-e"); + cmd.AddArg("class"); + cmd.AddArg(className); + } + cmd.AddArg(packageName + "/" + runner); + + print_command(cmd); + + int fds[2]; + pipe(fds); + + pid_t pid = fork(); + + if (pid == -1) { + // fork error + return errno; + } else if (pid == 0) { + // child + while ((dup2(fds[1], STDOUT_FILENO) == -1) && (errno == EINTR)) {} + close(fds[1]); + close(fds[0]); + const char* prog = cmd.GetProg(); + char* const* argv = cmd.GetArgv(); + char* const* env = cmd.GetEnv(); + execvpe(prog, argv, env); + print_error("Unable to run command: %s", prog); + exit(1); + } else { + // parent + close(fds[1]); + string result; + const int size = 16*1024; + char* buf = (char*)malloc(size); + int err = read_instrumentation_results(fds[0], buf, size, callbacks); + free(buf); + int status; + waitpid(pid, &status, 0); + if (err != 0) { + return err; + } + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } else { + return -1; + } + } +} + +/** + * Get the second to last bundle in the args list. Stores the last name found + * in last. If the path is not found or if the args list is empty, returns NULL. + */ +static const ResultsBundleEntry * +find_penultimate_entry(const ResultsBundle& bundle, va_list args) +{ + const ResultsBundle* b = &bundle; + const char* arg = va_arg(args, char*); + while (arg) { + string last = arg; + arg = va_arg(args, char*); + bool found = false; + for (int i=0; i<b->entries_size(); i++) { + const ResultsBundleEntry& e = b->entries(i); + if (e.key() == last) { + if (arg == NULL) { + return &e; + } else if (e.has_value_bundle()) { + b = &e.value_bundle(); + found = true; + } + } + } + if (!found) { + return NULL; + } + if (arg == NULL) { + return NULL; + } + } + return NULL; +} + +string +get_bundle_string(const ResultsBundle& bundle, bool* found, ...) +{ + va_list args; + va_start(args, found); + const ResultsBundleEntry* entry = find_penultimate_entry(bundle, args); + va_end(args); + if (entry == NULL) { + *found = false; + return string(); + } + if (entry->has_value_string()) { + *found = true; + return entry->value_string(); + } + *found = false; + return string(); +} + +int32_t +get_bundle_int(const ResultsBundle& bundle, bool* found, ...) +{ + va_list args; + va_start(args, found); + const ResultsBundleEntry* entry = find_penultimate_entry(bundle, args); + va_end(args); + if (entry == NULL) { + *found = false; + return 0; + } + if (entry->has_value_int()) { + *found = true; + return entry->value_int(); + } + *found = false; + return 0; +} + +float +get_bundle_float(const ResultsBundle& bundle, bool* found, ...) +{ + va_list args; + va_start(args, found); + const ResultsBundleEntry* entry = find_penultimate_entry(bundle, args); + va_end(args); + if (entry == NULL) { + *found = false; + return 0; + } + if (entry->has_value_float()) { + *found = true; + return entry->value_float(); + } + *found = false; + return 0; +} + +double +get_bundle_double(const ResultsBundle& bundle, bool* found, ...) +{ + va_list args; + va_start(args, found); + const ResultsBundleEntry* entry = find_penultimate_entry(bundle, args); + va_end(args); + if (entry == NULL) { + *found = false; + return 0; + } + if (entry->has_value_double()) { + *found = true; + return entry->value_double(); + } + *found = false; + return 0; +} + +int64_t +get_bundle_long(const ResultsBundle& bundle, bool* found, ...) +{ + va_list args; + va_start(args, found); + const ResultsBundleEntry* entry = find_penultimate_entry(bundle, args); + va_end(args); + if (entry == NULL) { + *found = false; + return 0; + } + if (entry->has_value_long()) { + *found = true; + return entry->value_long(); + } + *found = false; + return 0; +} + diff --git a/tools/bit/adb.h b/tools/bit/adb.h new file mode 100644 index 000000000000..dca80c853b45 --- /dev/null +++ b/tools/bit/adb.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ADB_H +#define ADB_H + +#include "instrumentation_data.pb.h" + +#include <string> + +using namespace android::am; +using namespace google::protobuf; +using namespace std; + +class InstrumentationCallbacks { +public: + virtual void OnTestStatus(TestStatus& status) = 0; + virtual void OnSessionStatus(SessionStatus& status) = 0; +}; + +int run_adb(const char* first, ...); + +string get_system_property(const string& name, int* err); + +int run_instrumentation_test(const string& packageName, const string& runner, + const string& className, InstrumentationCallbacks* callbacks); + +string get_bundle_string(const ResultsBundle& bundle, bool* found, ...); +int32_t get_bundle_int(const ResultsBundle& bundle, bool* found, ...); +float get_bundle_float(const ResultsBundle& bundle, bool* found, ...); +double get_bundle_double(const ResultsBundle& bundle, bool* found, ...); +int64_t get_bundle_long(const ResultsBundle& bundle, bool* found, ...); + +#endif // ADB_H diff --git a/tools/bit/command.cpp b/tools/bit/command.cpp new file mode 100644 index 000000000000..c5c12b4fad72 --- /dev/null +++ b/tools/bit/command.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "command.h" + +#include "print.h" +#include "util.h" + +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> + +Command::Command(const string& prog) + :prog(prog) +{ +} + +Command::~Command() +{ +} + +void +Command::AddArg(const string& arg) +{ + args.push_back(arg); +} + +void +Command::AddEnv(const string& name, const string& value) +{ + env[name] = value; +} + +const char* +Command::GetProg() const +{ + return prog.c_str(); +} + +char *const * +Command::GetArgv() const +{ + const int N = args.size(); + char** result = (char**)malloc(sizeof(char*)*(N+2)); + result[0] = strdup(prog.c_str()); + for (int i=0; i<N; i++) { + result[i+1] = strdup(args[i].c_str()); + } + result[N+1] = 0; + return result; +} + +char *const * +Command::GetEnv() const +{ + map<string,string> copy; + for (const char** p=(const char**)environ; *p != NULL; p++) { + char* name = strdup(*p); + char* value = strchr(name, '='); + *value = '\0'; + value++; + copy[name] = value; + free(name); + } + for (map<string,string>::const_iterator it=env.begin(); it!=env.end(); it++) { + copy[it->first] = it->second; + } + char** result = (char**)malloc(sizeof(char*)*(copy.size()+1)); + char** row = result; + for (map<string,string>::const_iterator it=copy.begin(); it!=copy.end(); it++) { + *row = (char*)malloc(it->first.size() + it->second.size() + 2); + strcpy(*row, it->first.c_str()); + strcat(*row, "="); + strcat(*row, it->second.c_str()); + row++; + } + *row = NULL; + return result; +} + +string +get_command_output(const Command& command, int* err, bool quiet) +{ + if (!quiet) { + print_command(command); + } + + int fds[2]; + pipe(fds); + + pid_t pid = fork(); + + if (pid == -1) { + // fork error + *err = errno; + return string(); + } else if (pid == 0) { + // child + while ((dup2(fds[1], STDOUT_FILENO) == -1) && (errno == EINTR)) {} + close(fds[1]); + close(fds[0]); + const char* prog = command.GetProg(); + char* const* argv = command.GetArgv(); + char* const* env = command.GetEnv(); + execvpe(prog, argv, env); + if (!quiet) { + print_error("Unable to run command: %s", prog); + } + exit(1); + } else { + // parent + close(fds[1]); + string result; + const int size = 16*1024; + char* buf = (char*)malloc(size); + while (true) { + ssize_t amt = read(fds[0], buf, size); + if (amt <= 0) { + break; + } else if (amt > 0) { + result.append(buf, amt); + } + } + free(buf); + int status; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) { + *err = WEXITSTATUS(status); + return result; + } else { + *err = -1; + return string(); + } + } +} + + +int +run_command(const Command& command) +{ + print_command(command); + + pid_t pid = fork(); + + if (pid == -1) { + // fork error + return errno; + } else if (pid == 0) { + // child + const char* prog = command.GetProg(); + char* const* argv = command.GetArgv(); + char* const* env = command.GetEnv(); + execvpe(prog, argv, env); + print_error("Unable to run command: %s", prog); + exit(1); + } else { + // parent + int status; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } else { + return -1; + } + } +} + diff --git a/tools/bit/command.h b/tools/bit/command.h new file mode 100644 index 000000000000..eb0b88f8d1ac --- /dev/null +++ b/tools/bit/command.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMMAND_H +#define COMMAND_H + +#include <map> +#include <string> +#include <vector> + +using namespace std; + +struct Command +{ + Command(const string& prog); + ~Command(); + + void AddArg(const string& arg); + void AddEnv(const string& name, const string& value); + + const char* GetProg() const; + char* const* GetArgv() const; + char* const* GetEnv() const; + + string GetCommandline() const; + + string prog; + vector<string> args; + map<string,string> env; +}; + +/** + * Run the command and collect stdout. + * Returns the exit code. + */ +string get_command_output(const Command& command, int* err, bool quiet=false); + +/** + * Run the command. + * Returns the exit code. + */ +int run_command(const Command& command); + +#endif // COMMAND_H + diff --git a/tools/bit/main.cpp b/tools/bit/main.cpp new file mode 100644 index 000000000000..04836adf2288 --- /dev/null +++ b/tools/bit/main.cpp @@ -0,0 +1,981 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aapt.h" +#include "adb.h" +#include "make.h" +#include "print.h" +#include "util.h" + +#include <sstream> +#include <string> +#include <vector> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <google/protobuf/stubs/common.h> + +using namespace std; + +/** + * An entry from the command line for something that will be built, installed, + * and/or tested. + */ +struct Target { + bool build; + bool install; + bool test; + string pattern; + string name; + vector<string> actions; + Module module; + + int testActionCount; + + int testPassCount; + int testFailCount; + bool actionsWithNoTests; + + Target(bool b, bool i, bool t, const string& p); +}; + +Target::Target(bool b, bool i, bool t, const string& p) + :build(b), + install(i), + test(t), + pattern(p), + testActionCount(0), + testPassCount(0), + testFailCount(0), + actionsWithNoTests(false) +{ +} + +/** + * Command line options. + */ +struct Options { + // For help + bool runHelp; + + // For tab completion + bool runTab; + string tabPattern; + + // For build/install/test + bool reboot; + vector<Target*> targets; + + Options(); + ~Options(); +}; + +Options::Options() + :runHelp(false), + runTab(false), + reboot(false), + targets() +{ +} + +Options::~Options() +{ +} + +struct InstallApk +{ + TrackedFile file; + bool alwaysInstall; + bool installed; + + InstallApk(); + InstallApk(const InstallApk& that); + InstallApk(const string& filename, bool always); + ~InstallApk() {}; +}; + +InstallApk::InstallApk() +{ +} + +InstallApk::InstallApk(const InstallApk& that) + :file(that.file), + alwaysInstall(that.alwaysInstall), + installed(that.installed) +{ +} + +InstallApk::InstallApk(const string& filename, bool always) + :file(filename), + alwaysInstall(always), + installed(false) +{ +} + + +/** + * Record for an test that is going to be launched. + */ +struct TestAction { + TestAction(); + + // The package name from the apk + string packageName; + + // The test runner class + string runner; + + // The test class, or none if all tests should be run + string className; + + // The original target that requested this action + Target* target; + + // The number of tests that passed + int passCount; + + // The number of tests that failed + int failCount; +}; + +TestAction::TestAction() + :passCount(0), + failCount(0) +{ +} + +/** + * Record for an activity that is going to be launched. + */ +struct ActivityAction { + // The package name from the apk + string packageName; + + // The test class, or none if all tests should be run + string className; +}; + +/** + * Callback class for the am instrument command. + */ +class TestResults: public InstrumentationCallbacks +{ +public: + virtual void OnTestStatus(TestStatus& status); + virtual void OnSessionStatus(SessionStatus& status); + + /** + * Set the TestAction that the tests are for. + * It will be updated with statistics as the tests run. + */ + void SetCurrentAction(TestAction* action); + +private: + TestAction* m_currentAction; +}; + +void +TestResults::OnTestStatus(TestStatus& status) +{ + bool found; +// printf("OnTestStatus\n"); +// status.PrintDebugString(); + int32_t resultCode = status.has_results() ? status.result_code() : 0; + + if (!status.has_results()) { + return; + } + const ResultsBundle &results = status.results(); + + int32_t currentTestNum = get_bundle_int(results, &found, "current", NULL); + if (!found) { + currentTestNum = -1; + } + + int32_t testCount = get_bundle_int(results, &found, "numtests", NULL); + if (!found) { + testCount = -1; + } + + string className = get_bundle_string(results, &found, "class", NULL); + if (!found) { + return; + } + + string testName = get_bundle_string(results, &found, "test", NULL); + if (!found) { + return; + } + + if (resultCode == 0) { + // test passed + m_currentAction->passCount++; + m_currentAction->target->testPassCount++; + } else if (resultCode == 1) { + // test starting + ostringstream line; + line << "Running"; + if (currentTestNum > 0) { + line << ": " << currentTestNum; + if (testCount > 0) { + line << " of " << testCount; + } + } + line << ": " << m_currentAction->target->name << ':' << className << "\\#" << testName; + print_one_line("%s", line.str().c_str()); + } else if (resultCode == -2) { + // test failed + m_currentAction->failCount++; + m_currentAction->target->testFailCount++; + printf("%s\n%sFailed: %s:%s\\#%s%s\n", g_escapeClearLine, g_escapeRedBold, + m_currentAction->target->name.c_str(), className.c_str(), + testName.c_str(), g_escapeEndColor); + + string stack = get_bundle_string(results, &found, "stack", NULL); + if (found) { + printf("%s\n", stack.c_str()); + } + } +} + +void +TestResults::OnSessionStatus(SessionStatus& /*status*/) +{ + //status.PrintDebugString(); +} + +void +TestResults::SetCurrentAction(TestAction* action) +{ + m_currentAction = action; +} + +/** + * Prints the usage statement / help text. + */ +static void +print_usage(FILE* out) { + fprintf(out, "usage: bit OPTIONS PATTERN\n"); + fprintf(out, "\n"); + fprintf(out, " Build, sync and test android code.\n"); + fprintf(out, "\n"); + fprintf(out, " The -b -i and -t options allow you to specify which phases\n"); + fprintf(out, " you want to run. If none of those options are given, then\n"); + fprintf(out, " all phases are run. If any of these options are provided\n"); + fprintf(out, " then only the listed phases are run.\n"); + fprintf(out, "\n"); + fprintf(out, " OPTIONS\n"); + fprintf(out, " -b Run a build\n"); + fprintf(out, " -i Install the targets\n"); + fprintf(out, " -t Run the tests\n"); + fprintf(out, "\n"); + fprintf(out, " -r If the runtime needs to be restarted, do a full reboot\n"); + fprintf(out, " instead\n"); + fprintf(out, "\n"); + fprintf(out, " PATTERN\n"); + fprintf(out, " One or more targets to build, install and test. The target\n"); + fprintf(out, " names are the names that appear in the LOCAL_MODULE or\n"); + fprintf(out, " LOCAL_PACKAGE_NAME variables in Android.mk or Android.bp files.\n"); + fprintf(out, "\n"); + fprintf(out, " Building and installing\n"); + fprintf(out, " -----------------------\n"); + fprintf(out, " The modules specified will be built and then installed. If the\n"); + fprintf(out, " files are on the system partition, they will be synced and the\n"); + fprintf(out, " attached device rebooted. If they are APKs that aren't on the\n"); + fprintf(out, " system partition they are installed with adb install.\n"); + fprintf(out, "\n"); + fprintf(out, " For example:\n"); + fprintf(out, " bit framework\n"); + fprintf(out, " Builds framework.jar, syncs the system partition and reboots.\n"); + fprintf(out, "\n"); + fprintf(out, " bit SystemUI\n"); + fprintf(out, " Builds SystemUI.apk, syncs the system partition and reboots.\n"); + fprintf(out, "\n"); + fprintf(out, " bit CtsProtoTestCases\n"); + fprintf(out, " Builds this CTS apk, adb installs it, but does not run any\n"); + fprintf(out, " tests.\n"); + fprintf(out, "\n"); + fprintf(out, " Running Unit Tests\n"); + fprintf(out, " ------------------\n"); + fprintf(out, " To run a unit test, list the test class names and optionally the\n"); + fprintf(out, " test method after the module.\n"); + fprintf(out, "\n"); + fprintf(out, " For example:\n"); + fprintf(out, " bit CtsProtoTestCases:*\n"); + fprintf(out, " Builds this CTS apk, adb installs it, and runs all the tests\n"); + fprintf(out, " contained in that apk.\n"); + fprintf(out, "\n"); + fprintf(out, " bit framework CtsProtoTestCases:*\n"); + fprintf(out, " Builds the framework and the apk, syncs and reboots, then\n"); + fprintf(out, " adb installs CtsProtoTestCases.apk, and runs all tests \n"); + fprintf(out, " contained in that apk.\n"); + fprintf(out, "\n"); + fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest\n"); + fprintf(out, " bit CtsProtoTestCases:android.util.proto.cts.ProtoOutputStreamBoolTest\n"); + fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs all the\n"); + fprintf(out, " tests in the ProtoOutputStreamBoolTest class.\n"); + fprintf(out, "\n"); + fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest\\#testWrite\n"); + fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs the testWrite\n"); + fprintf(out, " test method on that class.\n"); + fprintf(out, "\n"); + fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest\\#testWrite,.ProtoOutputStreamBoolTest\\#testRepeated\n"); + fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs the testWrite\n"); + fprintf(out, " and testRepeated test methods on that class.\n"); + fprintf(out, "\n"); + fprintf(out, " Launching an Activity\n"); + fprintf(out, " ---------------------\n"); + fprintf(out, " To launch an activity, specify the activity class name after\n"); + fprintf(out, " the module name.\n"); + fprintf(out, "\n"); + fprintf(out, " For example:\n"); + fprintf(out, " bit StatusBarTest:NotificationBuilderTest\n"); + fprintf(out, " bit StatusBarTest:.NotificationBuilderTest\n"); + fprintf(out, " bit StatusBarTest:com.android.statusbartest.NotificationBuilderTest\n"); + fprintf(out, " Builds and installs StatusBarTest.apk, launches the\n"); + fprintf(out, " com.android.statusbartest/.NotificationBuilderTest activity.\n"); + fprintf(out, "\n"); + fprintf(out, "\n"); + fprintf(out, "usage: bit --tab ...\n"); + fprintf(out, "\n"); + fprintf(out, " Lists the targets in a format for tab completion. To get tab\n"); + fprintf(out, " completion, add this to your bash environment:\n"); + fprintf(out, "\n"); + fprintf(out, " complete -C \"bit --tab\" bit\n"); + fprintf(out, "\n"); + fprintf(out, " Sourcing android's build/envsetup.sh will do this for you\n"); + fprintf(out, " automatically.\n"); + fprintf(out, "\n"); + fprintf(out, "\n"); + fprintf(out, "usage: bit --help\n"); + fprintf(out, "usage: bit -h\n"); + fprintf(out, "\n"); + fprintf(out, " Print this help message\n"); + fprintf(out, "\n"); +} + + +/** + * Sets the appropriate flag* variables. If there is a problem with the + * commandline arguments, prints the help message and exits with an error. + */ +static void +parse_args(Options* options, int argc, const char** argv) +{ + // Help + if (argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { + options->runHelp = true; + return; + } + + // Tab + if (argc >= 4 && strcmp(argv[1], "--tab") == 0) { + options->runTab = true; + options->tabPattern = argv[3]; + return; + } + + // Normal usage + bool anyPhases = false; + bool gotPattern = false; + bool flagBuild = false; + bool flagInstall = false; + bool flagTest = false; + for (int i=1; i < argc; i++) { + string arg(argv[i]); + if (arg[0] == '-') { + for (size_t j=1; j<arg.size(); j++) { + switch (arg[j]) { + case '-': + break; + case 'b': + if (gotPattern) { + gotPattern = false; + flagInstall = false; + flagTest = false; + } + flagBuild = true; + anyPhases = true; + break; + case 'i': + if (gotPattern) { + gotPattern = false; + flagBuild = false; + flagTest = false; + } + flagInstall = true; + anyPhases = true; + break; + case 't': + if (gotPattern) { + gotPattern = false; + flagBuild = false; + flagInstall = false; + } + flagTest = true; + anyPhases = true; + break; + case 'r': + options->reboot = true; + break; + default: + fprintf(stderr, "Unrecognized option '%c'\n", arg[j]); + print_usage(stderr); + exit(1); + break; + } + } + } else { + Target* target = new Target(flagBuild || !anyPhases, flagInstall || !anyPhases, + flagTest || !anyPhases, arg); + size_t colonPos = arg.find(':'); + if (colonPos == 0) { + fprintf(stderr, "Test / activity supplied without a module to build: %s\n", + arg.c_str()); + print_usage(stderr); + exit(1); + } else if (colonPos == string::npos) { + target->name = arg; + } else { + target->name.assign(arg, 0, colonPos); + size_t beginPos = colonPos+1; + size_t commaPos; + while (true) { + commaPos = arg.find(',', beginPos); + if (commaPos == string::npos) { + if (beginPos != arg.size()) { + target->actions.push_back(string(arg, beginPos, commaPos)); + } + break; + } else { + if (commaPos != beginPos) { + target->actions.push_back(string(arg, beginPos, commaPos-beginPos)); + } + beginPos = commaPos+1; + } + } + } + options->targets.push_back(target); + gotPattern = true; + } + } + // If no pattern was supplied, give an error + if (options->targets.size() == 0) { + fprintf(stderr, "No PATTERN supplied.\n\n"); + print_usage(stderr); + exit(1); + } +} + +/** + * Get an environment variable. + * Exits with an error if it is unset or the empty string. + */ +static string +get_required_env(const char* name, bool quiet) +{ + const char* value = getenv(name); + if (value == NULL || value[0] == '\0') { + if (!quiet) { + fprintf(stderr, "%s not set. Did you source build/envsetup.sh," + " run lunch and do a build?\n", name); + } + exit(1); + } + return string(value); +} + +/** + * Get the out directory. + * + * This duplicates the logic in build/make/core/envsetup.mk (which hasn't changed since 2011) + * so that we don't have to wait for get_build_var make invocation. + */ +string +get_out_dir() +{ + const char* out_dir = getenv("OUT_DIR"); + if (out_dir == NULL || out_dir[0] == '\0') { + const char* common_base = getenv("OUT_DIR_COMMON_BASE"); + if (common_base == NULL || common_base[0] == '\0') { + // We don't prefix with buildTop because we cd there and it + // makes all the filenames long when being pretty printed. + return "out"; + } else { + char* pwd = get_current_dir_name(); + const char* slash = strrchr(pwd, '/'); + if (slash == NULL) { + slash = ""; + } + string result(common_base); + result += slash; + free(pwd); + return result; + } + } + return string(out_dir); +} + +/** + * Check that a system property on the device matches the expected value. + * Exits with an error if they don't. + */ +static void +check_device_property(const string& property, const string& expected) +{ + int err; + string deviceValue = get_system_property(property, &err); + check_error(err); + if (deviceValue != expected) { + print_error("There is a mismatch between the build you just did and the device you"); + print_error("are trying to sync it to in the %s system property", property.c_str()); + print_error(" build: %s", expected.c_str()); + print_error(" device: %s", deviceValue.c_str()); + exit(1); + } +} + +/** + * Run the build, install, and test actions. + */ +void +run_phases(vector<Target*> targets, bool reboot) +{ + int err = 0; + + // + // Initialization + // + + print_status("Initializing"); + + const string buildTop = get_required_env("ANDROID_BUILD_TOP", false); + const string buildProduct = get_required_env("TARGET_PRODUCT", false); + const string buildVariant = get_required_env("TARGET_BUILD_VARIANT", false); + const string buildType = get_required_env("TARGET_BUILD_TYPE", false); + const string buildDevice = get_build_var(buildTop, "TARGET_DEVICE", false); + const string buildId = get_build_var(buildTop, "BUILD_ID", false); + const string buildOut = get_out_dir(); + + // TODO: print_command("cd", buildTop.c_str()); + chdir(buildTop.c_str()); + + // Get the modules for the targets + map<string,Module> modules; + read_modules(buildOut, buildDevice, &modules, false); + for (size_t i=0; i<targets.size(); i++) { + Target* target = targets[i]; + map<string,Module>::iterator mod = modules.find(target->name); + if (mod != modules.end()) { + target->module = mod->second; + } else { + print_error("Error: Could not find module: %s", target->name.c_str()); + err = 1; + } + } + if (err != 0) { + exit(1); + } + + // Choose the goals + vector<string> goals; + for (size_t i=0; i<targets.size(); i++) { + Target* target = targets[i]; + if (target->build) { + goals.push_back(target->name); + } + } + + // Figure out whether we need to sync the system and which apks to install + string systemPath = buildOut + "/target/product/" + buildDevice + "/system/"; + string dataPath = buildOut + "/target/product/" + buildDevice + "/data/"; + bool syncSystem = false; + bool alwaysSyncSystem = false; + vector<InstallApk> installApks; + for (size_t i=0; i<targets.size(); i++) { + Target* target = targets[i]; + if (target->install) { + for (size_t j=0; j<target->module.installed.size(); j++) { + const string& file = target->module.installed[j]; + // System partition + if (starts_with(file, systemPath)) { + syncSystem = true; + if (!target->build) { + // If a system partition target didn't get built then + // it won't change we will always need to do adb sync + alwaysSyncSystem = true; + } + continue; + } + // Apk in the data partition + if (starts_with(file, dataPath) && ends_with(file, ".apk")) { + // Always install it if we didn't build it because otherwise + // it will never have changed. + installApks.push_back(InstallApk(file, !target->build)); + continue; + } + } + } + } + map<string,FileInfo> systemFilesBefore; + if (syncSystem && !alwaysSyncSystem) { + get_directory_contents(systemPath, &systemFilesBefore); + } + + // + // Build + // + + // Run the build + if (goals.size() > 0) { + print_status("Building"); + err = build_goals(goals); + check_error(err); + } + + // + // Install + // + + // Sync the system partition and reboot + bool skipSync = false; + if (syncSystem) { + print_status("Syncing /system"); + + if (!alwaysSyncSystem) { + // If nothing changed and we weren't forced to sync, skip the reboot for speed. + map<string,FileInfo> systemFilesAfter; + get_directory_contents(systemPath, &systemFilesAfter); + skipSync = !directory_contents_differ(systemFilesBefore, systemFilesAfter); + } + if (skipSync) { + printf("Skipping sync because no files changed.\n"); + } else { + // Do some sanity checks + check_device_property("ro.build.product", buildProduct); + check_device_property("ro.build.type", buildVariant); + check_device_property("ro.build.id", buildId); + + // Stop & Sync + err = run_adb("shell", "stop", NULL); + check_error(err); + err = run_adb("remount", NULL); + check_error(err); + err = run_adb("sync", "system", NULL); + check_error(err); + + if (reboot) { + print_status("Rebooting"); + + err = run_adb("reboot", NULL); + check_error(err); + err = run_adb("wait-for-device", NULL); + check_error(err); + } else { + print_status("Restarting the runtime"); + + err = run_adb("shell", "setprop", "sys.boot_completed", "0", NULL); + check_error(err); + err = run_adb("shell", "start", NULL); + check_error(err); + } + + while (true) { + string completed = get_system_property("sys.boot_completed", &err); + check_error(err); + if (completed == "1") { + break; + } + sleep(2); + } + sleep(1); + err = run_adb("shell", "wm", "dismiss-keyguard", NULL); + check_error(err); + } + } + + // Install APKs + if (installApks.size() > 0) { + print_status("Installing APKs"); + for (size_t i=0; i<installApks.size(); i++) { + InstallApk& apk = installApks[i]; + if (!apk.file.fileInfo.exists || apk.file.HasChanged()) { + // It didn't exist before or it changed, so int needs install + err = run_adb("install", "-r", apk.file.filename.c_str(), NULL); + check_error(err); + apk.installed = true; + } else { + printf("APK didn't change. Skipping install of %s\n", apk.file.filename.c_str()); + } + } + } + + // + // Actions + // + + // Inspect the apks, and figure out what is an activity and what needs a test runner + bool printedInspecting = false; + vector<TestAction> testActions; + vector<ActivityAction> activityActions; + for (size_t i=0; i<targets.size(); i++) { + Target* target = targets[i]; + if (target->test) { + for (size_t j=0; j<target->module.installed.size(); j++) { + string filename = target->module.installed[j]; + + if (!ends_with(filename, ".apk")) { + continue; + } + + if (!printedInspecting) { + printedInspecting = true; + print_status("Inspecting APKs"); + } + + Apk apk; + err = inspect_apk(&apk, filename); + check_error(err); + + for (size_t k=0; k<target->actions.size(); k++) { + string actionString = target->actions[k]; + if (actionString == "*") { + if (apk.runner.length() == 0) { + print_error("Error: Test requested for apk that doesn't" + " have an <instrumentation> tag: %s\n", + target->module.name.c_str()); + exit(1); + } + TestAction action; + action.packageName = apk.package; + action.runner = apk.runner; + action.target = target; + testActions.push_back(action); + target->testActionCount++; + } else if (apk.HasActivity(actionString)) { + ActivityAction action; + action.packageName = apk.package; + action.className = full_class_name(apk.package, actionString); + activityActions.push_back(action); + } else { + if (apk.runner.length() == 0) { + print_error("Error: Test requested for apk that doesn't" + " have an <instrumentation> tag: %s\n", + target->module.name.c_str()); + exit(1); + } + TestAction action; + action.packageName = apk.package; + action.runner = apk.runner; + action.className = full_class_name(apk.package, actionString); + action.target = target; + testActions.push_back(action); + target->testActionCount++; + } + } + } + } + } + + // Run the instrumentation tests + TestResults testResults; + if (testActions.size() > 0) { + print_status("Running tests"); + for (size_t i=0; i<testActions.size(); i++) { + TestAction& action = testActions[i]; + testResults.SetCurrentAction(&action); + err = run_instrumentation_test(action.packageName, action.runner, action.className, + &testResults); + check_error(err); + if (action.passCount == 0 && action.failCount == 0) { + action.target->actionsWithNoTests = true; + } + int total = action.passCount + action.failCount; + printf("%sRan %d test%s for %s. ", g_escapeClearLine, + total, total > 1 ? "s" : "", action.target->name.c_str()); + if (action.passCount == 0 && action.failCount == 0) { + printf("%s%d passed, %d failed%s\n", g_escapeYellowBold, action.passCount, + action.failCount, g_escapeEndColor); + } else if (action.failCount > 0) { + printf("%d passed, %s%d failed%s\n", action.passCount, g_escapeRedBold, + action.failCount, g_escapeEndColor); + } else { + printf("%s%d passed%s, %d failed\n", g_escapeGreenBold, action.passCount, + g_escapeEndColor, action.failCount); + } + } + } + + // Launch the activity + if (activityActions.size() > 0) { + print_status("Starting activity"); + + if (activityActions.size() > 1) { + print_warning("Multiple activities specified. Will only start the first one:"); + for (size_t i=0; i<activityActions.size(); i++) { + ActivityAction& action = activityActions[i]; + print_warning(" %s", + pretty_component_name(action.packageName, action.className).c_str()); + } + } + + const ActivityAction& action = activityActions[0]; + string componentName = action.packageName + "/" + action.className; + err = run_adb("shell", "am", "start", componentName.c_str(), NULL); + check_error(err); + } + + // + // Print summary + // + + printf("\n%s--------------------------------------------%s\n", g_escapeBold, g_escapeEndColor); + + // Build + if (goals.size() > 0) { + printf("%sBuilt:%s\n", g_escapeBold, g_escapeEndColor); + for (size_t i=0; i<goals.size(); i++) { + printf(" %s\n", goals[i].c_str()); + } + } + + // Install + if (syncSystem) { + if (skipSync) { + printf("%sSkipped syncing /system partition%s\n", g_escapeBold, g_escapeEndColor); + } else { + printf("%sSynced /system partition%s\n", g_escapeBold, g_escapeEndColor); + } + } + if (installApks.size() > 0) { + bool printedTitle = false; + for (size_t i=0; i<installApks.size(); i++) { + const InstallApk& apk = installApks[i]; + if (apk.installed) { + if (!printedTitle) { + printf("%sInstalled:%s\n", g_escapeBold, g_escapeEndColor); + printedTitle = true; + } + printf(" %s\n", apk.file.filename.c_str()); + } + } + printedTitle = false; + for (size_t i=0; i<installApks.size(); i++) { + const InstallApk& apk = installApks[i]; + if (!apk.installed) { + if (!printedTitle) { + printf("%sSkipped install:%s\n", g_escapeBold, g_escapeEndColor); + printedTitle = true; + } + printf(" %s\n", apk.file.filename.c_str()); + } + } + } + + // Tests + if (testActions.size() > 0) { + printf("%sRan tests:%s\n", g_escapeBold, g_escapeEndColor); + size_t maxNameLength = 0; + for (size_t i=0; i<targets.size(); i++) { + Target* target = targets[i]; + if (target->test) { + size_t len = target->name.length(); + if (len > maxNameLength) { + maxNameLength = len; + } + } + } + string padding(maxNameLength, ' '); + for (size_t i=0; i<targets.size(); i++) { + Target* target = targets[i]; + if (target->testActionCount > 0) { + printf(" %s%s", target->name.c_str(), padding.c_str() + target->name.length()); + if (target->actionsWithNoTests) { + printf(" %s%d passed, %d failed%s\n", g_escapeYellowBold, + target->testPassCount, target->testFailCount, g_escapeEndColor); + } else if (target->testFailCount > 0) { + printf(" %d passed, %s%d failed%s\n", target->testPassCount, + g_escapeRedBold, target->testFailCount, g_escapeEndColor); + } else { + printf(" %s%d passed%s, %d failed\n", g_escapeGreenBold, + target->testPassCount, g_escapeEndColor, target->testFailCount); + } + } + } + } + if (activityActions.size() > 1) { + printf("%sStarted Activity:%s\n", g_escapeBold, g_escapeEndColor); + const ActivityAction& action = activityActions[0]; + printf(" %s\n", pretty_component_name(action.packageName, action.className).c_str()); + } + + printf("%s--------------------------------------------%s\n", g_escapeBold, g_escapeEndColor); +} + +/** + * Implement tab completion of the target names from the all modules file. + */ +void +run_tab_completion(const string& word) +{ + const string buildTop = get_required_env("ANDROID_BUILD_TOP", true); + const string buildProduct = get_required_env("TARGET_PRODUCT", false); + const string buildOut = get_out_dir(); + + chdir(buildTop.c_str()); + + string buildDevice = sniff_device_name(buildOut, buildProduct); + + map<string,Module> modules; + read_modules(buildOut, buildDevice, &modules, true); + + for (map<string,Module>::const_iterator it = modules.begin(); it != modules.end(); it++) { + if (starts_with(it->first, word)) { + printf("%s\n", it->first.c_str()); + } + } +} + +/** + * Main entry point. + */ +int +main(int argc, const char** argv) +{ + GOOGLE_PROTOBUF_VERIFY_VERSION; + init_print(); + + Options options; + parse_args(&options, argc, argv); + + if (options.runHelp) { + // Help + print_usage(stdout); + exit(0); + } else if (options.runTab) { + run_tab_completion(options.tabPattern); + exit(0); + } else { + // Normal run + run_phases(options.targets, options.reboot); + } + + return 0; +} + diff --git a/tools/bit/make.cpp b/tools/bit/make.cpp new file mode 100644 index 000000000000..60b5687bb313 --- /dev/null +++ b/tools/bit/make.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "make.h" + +#include "command.h" +#include "print.h" +#include "util.h" + +#include <json/reader.h> +#include <json/value.h> + +#include <fstream> +#include <string> +#include <map> + +#include <sys/types.h> +#include <dirent.h> +#include <string.h> + +using namespace std; + +map<string,string> g_buildVars; + +string +get_build_var(const string& buildTop, const string& name, bool quiet) +{ + int err; + + map<string,string>::iterator it = g_buildVars.find(name); + if (it == g_buildVars.end()) { + Command cmd("make"); + cmd.AddArg("--no-print-directory"); + cmd.AddArg("-C"); + cmd.AddArg(buildTop); + cmd.AddArg("-f"); + cmd.AddArg("build/core/config.mk"); + cmd.AddArg(string("dumpvar-") + name); + cmd.AddEnv("CALLED_FROM_SETUP", "true"); + cmd.AddEnv("BUILD_SYSTEM", "build/core"); + + string output = trim(get_command_output(cmd, &err, quiet)); + if (err == 0) { + g_buildVars[name] = output; + return output; + } else { + return string(); + } + } else { + return it->second; + } +} + +string +sniff_device_name(const string& buildOut, const string& product) +{ + string match("ro.build.product=" + product); + + string base(buildOut + "/target/product"); + DIR* dir = opendir(base.c_str()); + if (dir == NULL) { + return string(); + } + + dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (entry->d_type == DT_DIR) { + string filename(base + "/" + entry->d_name + "/system/build.prop"); + vector<string> lines; + split_lines(&lines, read_file(filename)); + for (size_t i=0; i<lines.size(); i++) { + if (lines[i] == match) { + return entry->d_name; + } + } + } + } + + closedir(dir); + return string(); +} + +void +json_error(const string& filename, const char* error, bool quiet) +{ + if (!quiet) { + print_error("Unable to parse module info file (%s): %s", error, filename.c_str()); + print_error("Have you done a full build?"); + } + exit(1); +} + +static void +get_values(const Json::Value& json, const string& name, vector<string>* result) +{ + Json::Value nullValue; + + const Json::Value& value = json.get(name, nullValue); + if (!value.isArray()) { + return; + } + + const int N = value.size(); + for (int i=0; i<N; i++) { + const Json::Value& child = value[i]; + if (child.isString()) { + result->push_back(child.asString()); + } + } +} + +void +read_modules(const string& buildOut, const string& device, map<string,Module>* result, bool quiet) +{ + string filename(string(buildOut + "/target/product/") + device + "/module-info.json"); + std::ifstream stream(filename, std::ifstream::binary); + + if (stream.fail()) { + if (!quiet) { + print_error("Unable to open module info file: %s", filename.c_str()); + print_error("Have you done a full build?"); + } + exit(1); + } + + Json::Value json; + Json::Reader reader; + if (!reader.parse(stream, json)) { + json_error(filename, "can't parse json format", quiet); + return; + } + + if (!json.isObject()) { + json_error(filename, "root element not an object", quiet); + return; + } + + vector<string> names = json.getMemberNames(); + const int N = names.size(); + for (int i=0; i<N; i++) { + const string& name = names[i]; + + const Json::Value& value = json[name]; + if (!value.isObject()) { + continue; + } + + Module module; + + module.name = name; + get_values(value, "class", &module.classes); + get_values(value, "path", &module.paths); + get_values(value, "installed", &module.installed); + + // Only keep classes we can handle + for (ssize_t i = module.classes.size() - 1; i >= 0; i--) { + string cl = module.classes[i]; + if (!(cl == "JAVA_LIBRARIES" || cl == "EXECUTABLES" || cl == "SHARED_LIBRARIES" + || cl == "APPS")) { + module.classes.erase(module.classes.begin() + i); + } + } + if (module.classes.size() == 0) { + continue; + } + + // Only target modules (not host) + for (ssize_t i = module.installed.size() - 1; i >= 0; i--) { + string fn = module.installed[i]; + if (!starts_with(fn, buildOut + "/target/")) { + module.installed.erase(module.installed.begin() + i); + } + } + if (module.installed.size() == 0) { + continue; + } + + (*result)[name] = module; + } +} + +int +build_goals(const vector<string>& goals) +{ + Command cmd("make"); + cmd.AddArg("-f"); + cmd.AddArg("build/core/main.mk"); + for (size_t i=0; i<goals.size(); i++) { + cmd.AddArg(goals[i]); + } + + return run_command(cmd); +} + diff --git a/tools/bit/make.h b/tools/bit/make.h new file mode 100644 index 000000000000..bb83c6e14226 --- /dev/null +++ b/tools/bit/make.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MAKE_H +#define MAKE_H + +#include <map> +#include <string> +#include <vector> + +using namespace std; + +struct Module +{ + string name; + vector<string> classes; + vector<string> paths; + vector<string> installed; +}; + +string get_build_var(const string& buildTop, const string& name, bool quiet); + +/** + * Poke around in the out directory and try to find a device name that matches + * our product. This is faster than running get_build_var and good enough for + * tab completion. + * + * Returns the empty string if we can't find one. + */ +string sniff_device_name(const string& buildOut, const string& product); + +void read_modules(const string& buildOut, const string& buildDevice, + map<string,Module>* modules, bool quiet); + +int build_goals(const vector<string>& goals); + +#endif // MAKE_H diff --git a/tools/bit/print.cpp b/tools/bit/print.cpp new file mode 100644 index 000000000000..790e0b4b227e --- /dev/null +++ b/tools/bit/print.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "print.h" + +#include <sys/ioctl.h> +#include <stdio.h> +#include <unistd.h> + +#include "util.h" + +bool g_stdoutIsTty; +char const* g_escapeBold; +char const* g_escapeRedBold; +char const* g_escapeGreenBold; +char const* g_escapeYellowBold; +char const* g_escapeUnderline; +char const* g_escapeEndColor; +char const* g_escapeClearLine; + +void +init_print() +{ + if (isatty(fileno(stdout))) { + g_stdoutIsTty = true; + g_escapeBold = "\033[1m"; + g_escapeRedBold = "\033[91m\033[1m"; + g_escapeGreenBold = "\033[92m\033[1m"; + g_escapeYellowBold = "\033[93m\033[1m"; + g_escapeUnderline = "\033[4m"; + g_escapeEndColor = "\033[0m"; + g_escapeClearLine = "\033[K"; + } else { + g_stdoutIsTty = false; + g_escapeBold = ""; + g_escapeRedBold = ""; + g_escapeGreenBold = ""; + g_escapeYellowBold = ""; + g_escapeUnderline = ""; + g_escapeEndColor = ""; + g_escapeClearLine = ""; + } +} + +void +print_status(const char* format, ...) +{ + printf("\n%s%s", g_escapeBold, g_escapeUnderline); + + va_list args; + va_start(args, format); + vfprintf(stdout, format, args); + va_end(args); + + printf("%s\n", g_escapeEndColor); +} + +void +print_command(const Command& command) +{ + fputs(g_escapeBold, stdout); + for (map<string,string>::const_iterator it=command.env.begin(); it!=command.env.end(); it++) { + fputs(it->first.c_str(), stdout); + fputc('=', stdout); + fputs(escape_for_commandline(it->second.c_str()).c_str(), stdout); + putc(' ', stdout); + } + fputs(command.prog.c_str(), stdout); + for (vector<string>::const_iterator it=command.args.begin(); it!=command.args.end(); it++) { + putc(' ', stdout); + fputs(escape_for_commandline(it->c_str()).c_str(), stdout); + } + fputs(g_escapeEndColor, stdout); + fputc('\n', stdout); +} + +void +print_error(const char* format, ...) +{ + fputs(g_escapeRedBold, stderr); + + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + + fputs(g_escapeEndColor, stderr); + fputc('\n', stderr); +} + +void +print_warning(const char* format, ...) +{ + fputs(g_escapeYellowBold, stderr); + + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + + fputs(g_escapeEndColor, stderr); + fputc('\n', stderr); +} + +void +print_one_line(const char* format, ...) +{ + if (g_stdoutIsTty) { + struct winsize ws; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + int size = ws.ws_col + 1; + char* buf = (char*)malloc(size); + + va_list args; + va_start(args, format); + vsnprintf(buf, size, format, args); + va_end(args); + + printf("%s%s\r", buf, g_escapeClearLine); + free(buf); + + fflush(stdout); + } else { + va_list args; + va_start(args, format); + vfprintf(stdout, format, args); + va_end(args); + printf("\n"); + } +} + +void +check_error(int err) +{ + if (err != 0) { + fputc('\n', stderr); + print_error("Stopping due to errors."); + exit(1); + } +} + + diff --git a/tools/bit/print.h b/tools/bit/print.h new file mode 100644 index 000000000000..b6c3e9aa27fa --- /dev/null +++ b/tools/bit/print.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PRINT_H +#define PRINT_H + +#include "command.h" + +extern bool g_stdoutIsTty; +extern char const* g_escapeBold; +extern char const* g_escapeRedBold; +extern char const* g_escapeGreenBold; +extern char const* g_escapeYellowBold; +extern char const* g_escapeUnderline; +extern char const* g_escapeEndColor; +extern char const* g_escapeClearLine; + +void init_print(); +void print_status(const char* format, ...); +void print_command(const Command& command); +void print_error(const char* format, ...); +void print_warning(const char* format, ...); +void print_one_line(const char* format, ...); +void check_error(int err); + +#endif // PRINT_H diff --git a/tools/bit/util.cpp b/tools/bit/util.cpp new file mode 100644 index 000000000000..fc93bcb8c935 --- /dev/null +++ b/tools/bit/util.cpp @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "util.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <dirent.h> +#include <string.h> +#include <unistd.h> + + +FileInfo::FileInfo() +{ + memset(this, 0, sizeof(FileInfo)); +} + +FileInfo::FileInfo(const FileInfo& that) +{ + memcpy(this, &that, sizeof(FileInfo)); +} + +FileInfo::FileInfo(const string& filename) +{ + struct stat st; + int err = stat(filename.c_str(), &st); + if (err != 0) { + memset(this, 0, sizeof(FileInfo)); + } else { + exists = true; + mtime = st.st_mtime; + ctime = st.st_ctime; + size = st.st_size; + } +} + +bool +FileInfo::operator==(const FileInfo& that) const +{ + return exists == that.exists + && mtime == that.mtime + && ctime == that.ctime + && size == that.size; +} + +bool +FileInfo::operator!=(const FileInfo& that) const +{ + return exists != that.exists + || mtime != that.mtime + || ctime != that.ctime + || size != that.size; +} + +FileInfo::~FileInfo() +{ +} + +TrackedFile::TrackedFile() + :filename(), + fileInfo() +{ +} + +TrackedFile::TrackedFile(const TrackedFile& that) +{ + filename = that.filename; + fileInfo = that.fileInfo; +} + +TrackedFile::TrackedFile(const string& file) + :filename(file), + fileInfo(file) +{ +} + +TrackedFile::~TrackedFile() +{ +} + +bool +TrackedFile::HasChanged() const +{ + FileInfo updated(filename); + return !updated.exists || fileInfo != updated; +} + +void +get_directory_contents(const string& name, map<string,FileInfo>* results) +{ + int err; + DIR* dir = opendir(name.c_str()); + if (dir == NULL) { + return; + } + + dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + if (entry->d_type == DT_DIR) { + string subdir = name + "/" + entry->d_name; + get_directory_contents(subdir, results); + } else if (entry->d_type == DT_LNK || entry->d_type == DT_REG) { + string filename(name + "/" + entry->d_name); + (*results)[filename] = FileInfo(filename); + } + } + + closedir(dir); +} + +bool +directory_contents_differ(const map<string,FileInfo>& before, const map<string,FileInfo>& after) +{ + if (before.size() != after.size()) { + return true; + } + map<string,FileInfo>::const_iterator b = before.begin(); + map<string,FileInfo>::const_iterator a = after.begin(); + while (b != before.end() && a != after.end()) { + if (b->first != a->first) { + return true; + } + if (a->second != b->second) { + return true; + } + a++; + b++; + } + return false; +} + +string +escape_quotes(const char* str) +{ + string result; + while (*str) { + if (*str == '"') { + result += '\\'; + result += '"'; + } else { + result += *str; + } + } + return result; +} + +string +escape_for_commandline(const char* str) +{ + if (strchr(str, '"') != NULL || strchr(str, ' ') != NULL + || strchr(str, '\t') != NULL) { + return escape_quotes(str); + } else { + return str; + } +} + +static bool +spacechr(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +string +trim(const string& str) +{ + const ssize_t N = (ssize_t)str.size(); + ssize_t begin = 0; + while (begin < N && spacechr(str[begin])) { + begin++; + } + ssize_t end = N - 1; + while (end >= begin && spacechr(str[end])) { + end--; + } + return string(str, begin, end-begin+1); +} + +bool +starts_with(const string& str, const string& prefix) +{ + return str.compare(0, prefix.length(), prefix) == 0; +} + +bool +ends_with(const string& str, const string& suffix) +{ + if (str.length() < suffix.length()) { + return false; + } else { + return str.compare(str.length()-suffix.length(), suffix.length(), suffix) == 0; + } +} + +void +split_lines(vector<string>* result, const string& str) +{ + const int N = str.length(); + int begin = 0; + int end = 0; + for (; end < N; end++) { + const char c = str[end]; + if (c == '\r' || c == '\n') { + if (begin != end) { + result->push_back(string(str, begin, end-begin)); + } + begin = end+1; + } + } + if (begin != end) { + result->push_back(string(str, begin, end-begin)); + } +} + +string +read_file(const string& filename) +{ + FILE* file = fopen(filename.c_str(), "r"); + if (file == NULL) { + return string(); + } + + fseek(file, 0, SEEK_END); + int size = ftell(file); + fseek(file, 0, SEEK_SET); + + char* buf = (char*)malloc(size); + fread(buf, 1, size, file); + + string result(buf, size); + + free(buf); + fclose(file); + + return result; +} + + diff --git a/tools/bit/util.h b/tools/bit/util.h new file mode 100644 index 000000000000..718f1474a969 --- /dev/null +++ b/tools/bit/util.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef UTIL_H +#define UTIL_H + +#include <map> +#include <string> +#include <vector> + +using namespace std; + +struct FileInfo +{ + bool exists; + time_t mtime; + time_t ctime; + off_t size; + + FileInfo(); + FileInfo(const FileInfo& that); + explicit FileInfo(const string& filename); + ~FileInfo(); + + bool operator==(const FileInfo& that) const; + bool operator!=(const FileInfo& that) const; +}; + + +/** + * Record for a file that we are watching + */ +struct TrackedFile { + string filename; + FileInfo fileInfo; + + TrackedFile(); + TrackedFile(const TrackedFile& that); + explicit TrackedFile(const string& filename); + ~TrackedFile(); + + // Returns if the file has changed. If it doesn't currently exist, returns true. + bool HasChanged() const; +}; + +/** + * Get FileInfo structures recursively for all the files and symlinks in a directory. + * Does not traverse symlinks, but it does record them. + */ +void get_directory_contents(const string& dir, map<string,FileInfo>* results); + +bool directory_contents_differ(const map<string,FileInfo>& before, + const map<string,FileInfo>& after); + +string escape_quotes(const char* str); + +string escape_for_commandline(const char* str); + +string trim(const string& trim); + +bool starts_with(const string& str, const string& prefix); + +bool ends_with(const string& str, const string& suffix); + +void split_lines(vector<string>* result, const string& str); + +string read_file(const string& filename); + +#endif // UTIL_H + |