#include "Builder.h" #include "file.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace bp = boost::process; namespace fs = std::filesystem; namespace pt = boost::property_tree; using namespace std::chrono_literals; using namespace std::string_literals; namespace { const std::string topelement{"ymake"}; bool is_match(const pt::ptree::value_type& element, const std::string& target) { if (target == "*") { return true; } else { return element.first == target; } } // get name of single target, ptree is subtree fs::path get_target(const pt::ptree& ptree) { return ptree.get("name"); } // ptree is main tree std::vector get_all_targets(const pt::ptree& ptree, const std::string& target) { std::vector result; // iterate over all elements for (const auto& element: ptree.get_child(topelement)) { if (is_match(element, target)) { fs::path target_name{element.second.get("name")}; result.push_back(target_name); // special case dynamic lib: add links automatically if (is_dynamic_lib(target_name)) { result.push_back(soname_short(target_name)); } } } return result; } // get sources of single target, ptree is subtree std::vector get_sources(const pt::ptree& ptree) { std::vector sources; for (const pt::ptree::value_type &v: ptree) { if (v.first == "source") sources.push_back(v.second.data()); } return sources; } // ptree is main tree std::vector get_all_sources(const pt::ptree& ptree, const std::string& target) { std::vector result; // iterate over all elements for (const auto& element: ptree.get_child(topelement)) { if (is_match(element, target)) { // iterate over all elements for (const auto& source: element.second) { if (source.first == "source") { result.push_back(source.second.data()); } } } } return result; } // get objects for corresponding sources of single target, // ptree is subtree std::vector get_objects(const pt::ptree& ptree) { std::vector objects{get_sources(ptree)}; for (auto &i: objects) { i.replace_extension("o"); } return objects; } // ptree is main tree std::vector get_all_objects(const pt::ptree& ptree, const std::string& target) { std::vector objects{get_all_sources(ptree, target)}; for (auto &i: objects) { i.replace_extension("o"); } return objects; } // both need to exist bool is_older(const fs::path& p, const fs::path& other) { auto t_p{fs::last_write_time(p)}; auto t_other{fs::last_write_time(other)}; return t_p < t_other; } std::vector deps_from_depfile(const fs::path& path) { std::string depfile_content{Reichwein::File::getFile(path)}; std::vector parts {Reichwein::Stringhelper::split(depfile_content, ":\r\n")}; if (parts.size() >= 2) { std::vector deps {Reichwein::Stringhelper::split(parts[1], " ")}; std::vector result; std::copy(deps.cbegin(), deps.cend(), std::back_inserter(result)); return result; } else { throw std::runtime_error("Bad depfile contents: "s + path.string()); } } fs::path depfile_name_from(const fs::path& p) { fs::path depfile{p}; depfile.replace_extension("d"); return depfile; } // T = std::string, fs::path template // get specified subelements (as list) of single target, ptree is subtree std::vector get_subelements_of_build(const pt::ptree& ptree, const std::string& subelement) { std::vector result; for (const pt::ptree::value_type &v: ptree) { if (v.first == subelement) { result.push_back(v.second.data()); } } return result; } // T = std::string, fs::path template std::unordered_map> get_subelements(const pt::ptree& ptree, const std::string& target, const std::string& subelement) { std::unordered_map> result_map; for (const auto& element: ptree.get_child(topelement)) { if (is_match(element, target)) { std::vector subelements{get_subelements_of_build(element.second, subelement)}; if (!subelements.empty()) { result_map.emplace(get_target(element.second), subelements); } } } return result_map; } } Builder::Builder(const pt::ptree& ptree, const std::string& target): _ptree(ptree), _target(target), _all_targets{get_all_targets(ptree, target)}, _all_objects{get_all_objects(ptree, target)}, _include_paths{get_subelements(ptree, target, "includepath")}, _link_libs{get_subelements(ptree, target, "linklib")} { // intentionally defer creation of _dependencies to build() // to prevent creation of .d files in clean() } // given a compiled compile unit (*.o), find its source file fs::path Builder::get_compile_unit_source_from_object(const fs::path& path) { std::vector source_files{dependencies_of(path)}; auto it{std::find_if(source_files.begin(), source_files.end(), is_compile_unit_source_by_extension)}; if (it == source_files.end()) { throw std::runtime_error(fmt::format("No source file found for {}", path.string())); } return *it; } std::vector Builder::dependencies_of(const fs::path& p) const { try { return _dependencies.at(p); } catch (const std::out_of_range& ex) { return {}; // empty by default } } std::vector Builder::link_libs_of(const fs::path& p) const { try { return _link_libs.at(p); } catch (const std::out_of_range& ex) { return {}; // empty by default } } fs::path Builder::target_from_object(const fs::path& object) const { for (const auto& target: _all_targets) { const auto& deps{dependencies_of(target)}; auto it{std::find(deps.cbegin(), deps.cend(), object)}; if (it != deps.cend()) { return target; } } throw std::runtime_error("No target found which depends on object "s + object.string()); } std::vector Builder::include_paths_of_object(const fs::path& object) const { fs::path target{target_from_object(object)}; // include paths from target try { return _include_paths.at(target); } catch (const std::out_of_range& ex) { return {}; // empty by default } } // outdated according to dependency tree, recursively bool Builder::is_outdated(const fs::path& p) const { if (!fs::exists(p)) return true; std::vector deps{dependencies_of(p)}; for (const auto& dep: deps) { if (!fs::exists(dep) || is_older(p, dep)) { return true; } if (is_outdated(dep)) { return true; } } return false; } // outdated according to dependency file list, non-recursively bool Builder::is_outdated(const fs::path& p, const std::vector &dependencies) const { if (!fs::exists(p)) return true; for (const auto& dep: dependencies) { if (!fs::exists(dep) || is_older(p, dep)) { return true; } } return false; } // return contained dependencies // input: cpp std::vector Builder::make_depfile_from(const fs::path& p) const { fs::path depfile{depfile_name_from(p)}; // check if depfile exists and if it contains up to date info if (!fs::exists(depfile) || is_outdated(depfile, deps_from_depfile(depfile))) { // actually create depfile int result{system(_lang.getDepCommand(depfile.string(), p.string()).c_str())}; if (result != 0) { throw std::runtime_error(fmt::format("Depfile {} can't be created", depfile.string())); } } return deps_from_depfile(depfile); } std::unordered_map> Builder::get_dependencies(const pt::ptree& ptree, bool include_sources) const { std::unordered_map> dependencies; for (const auto& element: ptree.get_child(topelement)) { if (is_match(element, _target)) { // add target const pt::ptree& list{element.second}; fs::path target{get_target(list)}; std::vector deps{get_objects(list)}; std::vector link_libs{link_libs_of(target)}; std::copy_if(link_libs.begin(), link_libs.end(), std::back_inserter(deps), [](const fs::path& p){ return !is_external_lib(p);}); dependencies.emplace(target, deps); // add dynamic lib links if (is_dynamic_lib(target)) { dependencies.emplace(soname_shorter(target), std::vector{{target}}); dependencies.emplace(soname_short(target), std::vector{{soname_shorter(target)}}); } if (include_sources) { // add source dependencies of *.o std::vector sources{get_sources(list)}; for (const auto& p: sources) { fs::path p_obj{p}; p_obj.replace_extension("o"); std::vector deps {make_depfile_from(p)}; // keep .d files for now to speed dependencies detection on following runs //fs::remove(depfile_name_from(p)); dependencies.emplace(p_obj, deps); } } } } // add dependencies to YMakefiles for (auto& i: dependencies) { if (is_buildable_by_name(i.first) && fs::exists(i.first.parent_path() / YMakefile)) { i.second.push_back(i.first.parent_path() / YMakefile); } } return dependencies; } // build 1 file void Builder::build_file(const fs::path& p) { std::string command; if (p.extension() == ".o") { // compile fs::path source_file{get_compile_unit_source_from_object(p)}; command = _lang.getCompileCommand(p, source_file, target_from_object(p), include_paths_of_object(p)); } else { // link std::vector deps{dependencies_of(p)}; std::vector objects; std::copy_if(deps.begin(), deps.end(), std::back_inserter(objects), is_linkable); std::vector link_libs{link_libs_of(p)}; command = _lang.getLinkCommand(p, objects, link_libs); } std::cout << command << std::endl; _runner.spawn(p.string(), command.c_str()); } void Builder::cleanup_buildlist() { std::string path; int exit_code{_runner.wait_one(path)}; _activelist.erase(fs::path{path}); _donelist.insert(fs::path{path}); if (exit_code != 0) { throw std::runtime_error(fmt::format("Exit code {}", exit_code)); } } // build list of files // eats up _buildlist void Builder::build_filelist() { if (_buildlist.empty()) { std::cout << "Everything up to date." << std::endl; } else { // std::cout << "Running commands: " << std::endl; } while (!_buildlist.empty()) { // find file which can be built directly since its build dependencies are up to date fs::path current; while (current.empty()) { for (auto &i: _buildlist) { std::vector deps{dependencies_of(i)}; bool deps_up_to_date{true}; for (auto& dep: deps) { if (_activelist.find(dep) != _activelist.end() || is_outdated(dep)) { deps_up_to_date = false; } } if (deps_up_to_date) { current = i; break; } } if (current.empty()) { std::this_thread::sleep_for(10ms); // short wait before retry if (_runner.finished() > 0) { cleanup_buildlist(); } } } _buildlist.erase(current); _activelist.insert(current); // wait until process slot is available while (_runner.full() || _runner.finished() > 0) { cleanup_buildlist(); } build_file(current); // calls spawn() on _runner } // final cleanup while (_runner.finished() != 0 || _runner.running() != 0) { cleanup_buildlist(); } if (!_activelist.empty()) { throw std::runtime_error("Files left actively building"); } } std::unordered_set Builder::get_buildlist(std::function outdated_pred) { std::unordered_set result; // create build list by depth-first search //std::cout << "Calculating build list..." << std::endl; std::stack container; // temporary container for search algorithm for (const auto& target: _all_targets) { container.push(target); } while (!container.empty()) { fs::path current{container.top()}; container.pop(); std::vector deps{dependencies_of(current)}; for (auto &i: deps) { container.push(i); } if (outdated_pred(current) && is_buildable_by_name(current)) { result.insert(current); } } return result; } // build everything according to specified configuration void Builder::build() { _dependencies = get_dependencies(_ptree); _buildlist = get_buildlist([&](const fs::path& i)->bool{return is_outdated(i);}); //std::cout << "Build list:" << std::endl; //for (auto &i: _buildlist) { // std::cout << " " << i << std::endl; //} build_filelist(); } void Builder::clean() { _dependencies = get_dependencies(_ptree, false); std::unordered_set cleanlist{get_buildlist([](const fs::path& i)->bool{ return true;})}; std::unordered_set add; for (const auto& i: cleanlist) { std::vector deps{dependencies_of(i)}; std::copy_if(deps.begin(), deps.end(), std::inserter(add, add.begin()), is_buildable_by_name); } std::copy(add.begin(), add.end(), std::inserter(cleanlist, cleanlist.begin())); std::vector commands; for (auto &i: cleanlist) { if (fs::exists(i)) { commands.push_back(fmt::format("rm -f {}", i.string())); } if (i.extension() == ".o") { fs::path depfile{depfile_name_from(i)}; if (fs::exists(depfile)) { commands.push_back(fmt::format("rm -f {}", depfile.string())); } } } //std::cout << "Running commands: " << std::endl; for (auto &i: commands) { std::cout << i << std::endl; int result{system(i.c_str())}; if (result != 0) { throw std::runtime_error(fmt::format("Error {}", result)); } } } // build tests and run them void Builder::run_tests() { for (const auto& i: _all_targets) { std::string command{(fs::path("./") / i).string()}; std::cout << "Running test: " << command << std::endl; int result {bp::system(command)}; if (result != 0) { throw std::runtime_error("Test failed: " + i.string() + ", exit code: " + std::to_string(result)); } } }