13#include "flutter/third_party/re2/re2/re2.h"
19#include "third_party/abseil-cpp/absl/container/btree_map.h"
20#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
21#include "third_party/abseil-cpp/absl/log/log.h"
22#include "third_party/abseil-cpp/absl/log/vlog_is_on.h"
23#include "third_party/abseil-cpp/absl/status/statusor.h"
24#include "third_party/abseil-cpp/absl/strings/str_cat.h"
26namespace fs = std::filesystem;
32const std::array<std::string_view, 9> kLicenseFileNames = {
33 "LICENSE",
"LICENSE.TXT",
"LICENSE.txt",
"LICENSE.md",
"LICENSE.MIT",
34 "COPYING",
"License.txt",
"docs/FTL.TXT",
"README.ijg"};
39const std::array<std::string_view, 2> kThirdPartyIgnore = {
"pkg",
44std::vector<fs::path> GetGitRepos(std::string_view dir) {
45 std::vector<fs::path> result;
46 for (
const fs::directory_entry& entry :
47 fs::recursive_directory_iterator(dir)) {
48 if (entry.path().stem() ==
".git") {
49 result.push_back(entry.path().parent_path());
54 if (!result.empty() && result[0] != dir) {
55 result.push_back(dir);
60absl::StatusOr<std::vector<std::string>> GitLsFiles(
const fs::path& repo_path) {
61 std::vector<std::string> files;
63 std::string cmd =
"git -C \"" + repo_path.string() +
"\" ls-files";
64 std::unique_ptr<FILE,
decltype(&pclose)> pipe(popen(cmd.c_str(),
"r"),
68 return absl::InvalidArgumentError(
"can't run git ls-files in " +
69 repo_path.lexically_normal().string());
73 while (fgets(buffer,
sizeof(buffer), pipe.get()) !=
nullptr) {
74 std::string line(buffer);
75 if (!line.empty() && line.back() ==
'\n') {
78 files.emplace_back(std::move(line));
84std::optional<fs::path> FindLicense(
const Data&
data,
85 const fs::path& working_dir,
86 const fs::path& relative_path) {
87 for (std::string_view license_name : kLicenseFileNames) {
88 fs::path relative_license_path =
89 (relative_path / license_name).lexically_normal();
90 fs::path full_license_path = working_dir / relative_license_path;
91 if (fs::exists(full_license_path) &&
92 !
data.exclude_filter.Matches(relative_license_path.string())) {
93 return full_license_path;
99void PrintProgress(
size_t idx,
size_t count) {
100 std::cout <<
"\rprogress: [";
101 double percent =
static_cast<double>(idx) / count;
102 int done = percent * 50;
103 int left = 50 - done;
104 for (
int i = 0;
i < done; ++
i) {
107 for (
int i = 0;
i <
left; ++
i) {
110 std::cout <<
"]" << std::flush;
113bool IsStdoutTerminal() {
114 return isatty(STDOUT_FILENO);
117class LicensesWriter {
119 explicit LicensesWriter(std::ostream& licenses) : licenses_(licenses) {}
121 void Write(
const absl::flat_hash_set<std::string>& packages,
122 std::string_view license) {
123 std::vector<std::string_view> sorted;
124 sorted.reserve(packages.size());
125 sorted.insert(sorted.end(), packages.begin(), packages.end());
126 std::sort(sorted.begin(), sorted.end());
128 for (
int i = 0;
i < 80; ++
i) {
133 first_write_ =
false;
134 for (std::string_view package : sorted) {
135 licenses_ <<
package << "\n";
137 licenses_ <<
"\n" << license <<
"\n";
141 std::ostream& licenses_;
142 bool first_write_ =
true;
147 std::optional<fs::path> license_file;
148 bool is_root_package;
154std::string GetDirFilename(
const fs::path& working_dir) {
155 std::string result = working_dir.filename();
156 if (result.empty()) {
157 result = working_dir.parent_path().filename();
163 const fs::path& working_dir,
164 const fs::path& relative_path,
168 : GetDirFilename(working_dir);
170 .name = root_package_name,
171 .license_file = FindLicense(
data, working_dir,
"."),
172 .is_root_package =
true,
174 bool after_third_party =
false;
175 bool after_ignored_third_party =
false;
176 fs::path current =
".";
177 for (
const fs::path& component : relative_path.parent_path()) {
178 current /= component;
179 std::optional<fs::path> current_license =
180 FindLicense(
data, working_dir, current);
181 if (current_license.has_value()) {
182 result.license_file = current_license;
184 if (after_ignored_third_party) {
185 after_ignored_third_party =
false;
186 result.name = component;
187 }
else if (after_third_party) {
188 if (std::find(kThirdPartyIgnore.begin(), kThirdPartyIgnore.end(),
189 component.string()) != kThirdPartyIgnore.end()) {
190 after_ignored_third_party =
true;
192 after_third_party =
false;
193 result.name = component;
194 }
else if (component.string() ==
"third_party") {
195 after_third_party =
true;
196 result.license_file = std::nullopt;
197 result.is_root_package =
false;
200 if (std::find(kLicenseFileNames.begin(), kLicenseFileNames.end(),
201 relative_path.filename()) != kLicenseFileNames.end()) {
202 result.license_file = working_dir / relative_path;
210 void Add(std::string_view package, std::string_view license) {
211 auto package_emplace_result =
212 map_.try_emplace(license, absl::flat_hash_set<std::string>());
213 absl::flat_hash_set<std::string>& comment_set =
214 package_emplace_result.first->second;
215 if (comment_set.find(package) == comment_set.end()) {
217 comment_set.emplace(std::string(package));
221 void Write(std::ostream& licenses) {
222 LicensesWriter writer(licenses);
223 for (
const auto& comment_entry : map_) {
224 writer.Write(comment_entry.second, comment_entry.first);
229 absl::btree_map<std::string, absl::flat_hash_set<std::string>> map_;
230 absl::flat_hash_set<std::string> license_files_;
241absl::Status MatchLicenseFile(
const fs::path& path,
242 const Package& package,
244 LicenseMap* license_map) {
245 if (!package.license_file.has_value()) {
246 return absl::InvalidArgumentError(
"No license file.");
250 return license.status();
252 absl::StatusOr<std::vector<Catalog::Match>> matches =
253 data.catalog.FindMatch(
254 std::string_view(license->GetData(), license->GetSize()));
258 license_map->Add(package.name, match.GetMatchedText());
259 VLOG(1) <<
"OK: " <<
path <<
" : " << match.GetMatcher();
262 return absl::NotFoundError(
263 absl::StrCat(
"Unknown license in ",
264 package.license_file->lexically_normal().string(),
" : ",
265 matches.status().message()));
268 return absl::OkStatus();
273 LicenseMap license_map;
274 std::vector<absl::Status> errors;
275 absl::flat_hash_set<fs::path> seen_license_files;
279bool ProcessSourceCode(
const fs::path& relative_path,
282 const Package& package,
284 ProcessState* state) {
285 bool did_find_copyright =
false;
286 std::vector<absl::Status>* errors = &state->errors;
287 LicenseMap* license_map = &state->license_map;
288 int32_t comment_count = 0;
290 auto comment_handler = [&](std::string_view comment) ->
void {
293 re2::StringPiece match;
294 if (RE2::PartialMatch(comment, kHeaderLicense, &match)) {
295 if (!VLOG_IS_ON(4)) {
298 absl::StatusOr<std::vector<Catalog::Match>> matches =
299 data.catalog.FindMatch(comment);
301 did_find_copyright =
true;
303 license_map->Add(package.name, match.GetMatchedText());
304 VLOG(1) <<
"OK: " << relative_path.lexically_normal() <<
" : "
305 << match.GetMatcher();
309 errors->push_back(absl::NotFoundError(
310 absl::StrCat(relative_path.lexically_normal().string(),
" : ",
311 matches.status().message(),
"\n", comment)));
313 VLOG(2) <<
"NOT_FOUND: " << relative_path.lexically_normal() <<
" : "
314 << matches.status().message() <<
"\n"
324 if (comment_count <= 0) {
325 comment_handler(std::string_view(file.
GetData(), file.
GetSize()));
328 return did_find_copyright;
331std::vector<std::string_view> SplitLines(std::string_view
input) {
332 std::vector<std::string_view> result;
336 while (pos <
input.size() &&
input[pos] ==
'\n') {
339 const ::std::string::size_type newline =
input.find(
'\n', pos);
340 if (newline == ::std::string::npos) {
341 result.push_back(
input.substr(pos));
344 result.push_back(
input.substr(pos, newline - pos));
352bool ProcessNotices(
const fs::path& relative_path,
355 const Package& package,
357 ProcessState* state) {
360 static const std::string kDelimitor =
361 "------------------------------------------------------------------------"
363 static const std::string pattern_str =
364 "(?s)(.+?)\n\n(.+?)(?:\n?" + kDelimitor +
"|$)";
365 static const RE2 regex(pattern_str);
367 std::vector<absl::Status>* errors = &state->errors;
368 LicenseMap* license_map = &state->license_map;
370 std::string_view projects_text;
371 std::string_view license;
372 while (RE2::FindAndConsume(&
input, regex, &projects_text, &license)) {
373 std::vector<std::string_view> projects = SplitLines(projects_text);
377 absl::StatusOr<std::vector<Catalog::Match>> matches =
378 data.catalog.FindMatch(license);
381 for (std::string_view project : projects) {
382 license_map->Add(project, match.GetMatchedText());
384 VLOG(1) <<
"OK: " << relative_path.lexically_normal() <<
" : "
385 << match.GetMatcher();
388 VLOG(2) <<
"NOT_FOUND: " << relative_path.lexically_normal() <<
" : "
389 << matches.status().message() <<
"\n"
392 errors->push_back(absl::NotFoundError(
393 absl::StrCat(relative_path.lexically_normal().string(),
" : ",
394 matches.status().message(),
"\n", license)));
404absl::Status ProcessFile(
const fs::path& working_dir_path,
405 std::ostream& licenses,
407 const fs::path& full_path,
409 ProcessState* state) {
410 std::vector<absl::Status>* errors = &state->errors;
411 LicenseMap* license_map = &state->license_map;
412 absl::flat_hash_set<fs::path>* seen_license_files =
413 &state->seen_license_files;
415 bool did_find_copyright =
false;
416 fs::path relative_path = full_path.lexically_relative(working_dir_path);
417 VLOG(2) <<
"Process: " << relative_path;
418 if (!
data.include_filter.Matches(relative_path.string()) ||
419 data.exclude_filter.Matches(relative_path.string())) {
420 VLOG(1) <<
"EXCLUDE: " << relative_path.lexically_normal();
421 return absl::OkStatus();
424 Package
package = GetPackage(data, working_dir_path, relative_path, flags);
425 if (package.license_file.has_value()) {
426 auto [_, is_new_item] =
427 seen_license_files->insert(package.license_file.value());
429 absl::Status match_status = MatchLicenseFile(package.license_file.value(),
430 package,
data, license_map);
431 if (!match_status.ok()) {
432 errors->emplace_back(std::move(match_status));
436 VLOG(3) <<
"No license file: " << relative_path.lexically_normal();
439 absl::StatusOr<MMapFile> file =
MMapFile::Make(full_path.string());
441 if (file.status().code() == absl::StatusCode::kInvalidArgument) {
443 return absl::OkStatus();
446 errors->push_back(file.status());
447 return file.status();
451 if (full_path.filename().string() ==
"NOTICES") {
453 ProcessNotices(relative_path, *file,
data, package, flags, state);
456 ProcessSourceCode(relative_path, *file,
data, package, flags, state);
458 if (!did_find_copyright) {
459 if (package.license_file.has_value()) {
460 if (package.is_root_package) {
462 absl::NotFoundError(
"Expected root copyright in " +
463 relative_path.lexically_normal().string()));
465 fs::path relative_license_path =
466 package.license_file->lexically_relative(working_dir_path);
467 VLOG(1) <<
"OK: " << relative_path.lexically_normal()
468 <<
" : dir license(" << relative_license_path.lexically_normal()
473 absl::NotFoundError(
"Expected copyright in " +
474 relative_path.lexically_normal().string()));
477 return absl::OkStatus();
483fs::path FindFileInParentDirectories(
const fs::path& starting_dir,
484 const std::string_view file_name) {
485 fs::path current_dir = fs::absolute(starting_dir);
486 while (!current_dir.empty() && current_dir != current_dir.root_path()) {
487 fs::path file_path = current_dir / file_name;
488 if (fs::exists(file_path)) {
491 current_dir = current_dir.parent_path();
498 std::ostream& licenses,
501 return Run(working_dir, licenses,
data, flags);
505 std::string_view working_dir,
506 std::ostream& licenses,
509 fs::path working_dir_path =
510 fs::absolute(fs::path(working_dir)).lexically_normal();
511 std::vector<fs::path> git_repos = GetGitRepos(working_dir_path.string());
519 fs::path deps_path = FindFileInParentDirectories(working_dir_path,
"DEPS");
520 if (!deps_path.empty()) {
521 absl::StatusOr<MMapFile> deps_file =
MMapFile::Make(deps_path.string());
522 if (deps_file.ok()) {
524 std::vector<std::string> deps = deps_parser.
Parse(
525 std::string_view(deps_file->GetData(), deps_file->GetSize()));
526 for (
const std::string& dep : deps) {
527 fs::path dep_path = deps_path.parent_path() / dep;
528 if (fs::is_directory(dep_path)) {
531 if (dep_path.string().find(working_dir_path.string()) != 0) {
535 for (
const auto& entry : fs::recursive_directory_iterator(dep_path)) {
536 if (entry.is_regular_file()) {
537 absl::Status process_result =
538 ProcessFile(working_dir_path, licenses,
data, entry.path(),
540 if (!process_result.ok()) {
550 for (
const fs::path& git_repo : git_repos) {
551 if (!VLOG_IS_ON(1) && IsStdoutTerminal()) {
552 PrintProgress(count++, git_repos.size());
555 absl::StatusOr<std::vector<std::string>> git_files = GitLsFiles(git_repo);
556 if (!git_files.ok()) {
557 state.errors.push_back(git_files.status());
560 for (
const std::string& git_file : git_files.value()) {
561 fs::path full_path = git_repo / git_file;
562 absl::Status process_result = ProcessFile(working_dir_path, licenses,
563 data, full_path, flags, &state);
564 if (!process_result.ok()) {
570 if (!
data.secondary_dir.empty()) {
571 for (
const auto& entry :
572 fs::recursive_directory_iterator(
data.secondary_dir)) {
573 if (!fs::is_directory(entry)) {
574 fs::path relative_path = fs::relative(entry,
data.secondary_dir);
575 if (!fs::exists(working_dir / relative_path.parent_path())) {
576 state.errors.push_back(absl::InvalidArgumentError(absl::StrCat(
577 "secondary license path mixmatch at ", relative_path.string())));
579 fs::path full_path =
data.secondary_dir / entry;
581 GetPackage(data, working_dir_path, relative_path, flags);
582 absl::StatusOr<MMapFile> file =
MMapFile::Make(full_path.string());
584 state.license_map.Add(
586 std::string_view(file->GetData(), file->GetSize()));
588 state.errors.push_back(file.status());
595 state.license_map.Write(licenses);
596 if (!VLOG_IS_ON(1) && IsStdoutTerminal()) {
597 PrintProgress(count++, git_repos.size());
598 std::cout << std::endl;
605 std::ostream& licenses,
606 std::string_view data_dir,
610 std::cerr <<
"Can't load data at " << data_dir <<
": " <<
data.status()
614 std::vector<absl::Status> errors =
615 Run(working_dir, licenses,
data.value(), flags);
616 for (
const absl::Status& status : errors) {
617 std::cerr << status <<
"\n";
620 if (!errors.empty()) {
621 std::cout <<
"Error count: " << errors.size();
624 return errors.empty() ? 0 : 1;
628 std::string_view full_path,
629 std::ostream& licenses,
630 std::string_view data_dir,
631 const Flags& flags) {
634 std::cerr <<
"Can't load data at " << data_dir <<
": " <<
data.status()
640 fs::path working_dir_path =
641 fs::absolute(fs::path(working_dir)).lexically_normal();
642 fs::path absolute_full_path = fs::absolute(fs::path(full_path));
643 absl::Status process_result =
644 ProcessFile(working_dir_path, licenses,
data.value(), absolute_full_path,
647 if (!process_result.ok()) {
648 std::cerr << process_result << std::endl;
652 state.license_map.Write(licenses);
653 for (
const absl::Status& status : state.errors) {
654 std::cerr << status <<
"\n";
657 if (!state.errors.empty()) {
658 std::cout <<
"Error count: " << state.errors.size();
661 return state.errors.empty() ? 0 : 1;
std::vector< std::string > Parse(std::string_view input)
static const char * kHeaderLicenseRegex
static int FileRun(std::string_view working_dir, std::string_view full_path, std::ostream &licenses, std::string_view data_dir, const Flags &flags)
Run on a single file.
static std::vector< absl::Status > Run(std::string_view working_dir, std::ostream &licenses, const Data &data)
static absl::StatusOr< MMapFile > Make(std::string_view path)
const char * GetData() const
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set profile Make the profiler discard new samples once the profiler sample buffer is full When this flag is not the profiler sample buffer is used as a ring buffer
static absl::StatusOr< Data > Open(std::string_view data_dir)
bool treat_unmatched_comments_as_errors
std::optional< std::string > root_package_name
std::shared_ptr< const fml::Mapping > data