Flutter Engine
 
Loading...
Searching...
No Matches
license_checker.cc
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
6
7#include <unistd.h>
8#include <filesystem>
9#include <fstream>
10#include <iostream>
11#include <vector>
12
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"
25
26namespace fs = std::filesystem;
27
28const char* LicenseChecker::kHeaderLicenseRegex = "(?i)(license|copyright)";
29
30namespace {
31// TODO(): Move this into the data directory.
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"};
35
36// TODO(): Move this into the data directory
37// These are directories that when they are found in third_party directories
38// are ignored as package names.
39const std::array<std::string_view, 2> kThirdPartyIgnore = {"pkg",
40 "vulkan-deps"};
41
42RE2 kHeaderLicense(LicenseChecker::kHeaderLicenseRegex);
43
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());
50 }
51 }
52 // Put the query dir in there if we didn't get it yet. This allows us to
53 // query subdirectories, like `engine`.
54 if (!result.empty() && result[0] != dir) {
55 result.push_back(dir);
56 }
57 return result;
58}
59
60absl::StatusOr<std::vector<std::string>> GitLsFiles(const fs::path& repo_path) {
61 std::vector<std::string> files;
62
63 std::string cmd = "git -C \"" + repo_path.string() + "\" ls-files";
64 std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"),
65 pclose);
66
67 if (!pipe) {
68 return absl::InvalidArgumentError("can't run git ls-files in " +
69 repo_path.lexically_normal().string());
70 }
71
72 char buffer[4096];
73 while (fgets(buffer, sizeof(buffer), pipe.get()) != nullptr) {
74 std::string line(buffer);
75 if (!line.empty() && line.back() == '\n') {
76 line.pop_back();
77 }
78 files.emplace_back(std::move(line));
79 }
80
81 return files;
82}
83
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;
94 }
95 }
96 return std::nullopt;
97}
98
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) {
105 std::cout << "o";
106 }
107 for (int i = 0; i < left; ++i) {
108 std::cout << ".";
109 }
110 std::cout << "]" << std::flush;
111}
112
113bool IsStdoutTerminal() {
114 return isatty(STDOUT_FILENO);
115}
116
117class LicensesWriter {
118 public:
119 explicit LicensesWriter(std::ostream& licenses) : licenses_(licenses) {}
120
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());
127 if (!first_write_) {
128 for (int i = 0; i < 80; ++i) {
129 licenses_.put('-');
130 }
131 licenses_.put('\n');
132 }
133 first_write_ = false;
134 for (std::string_view package : sorted) {
135 licenses_ << package << "\n";
136 }
137 licenses_ << "\n" << license << "\n";
138 }
139
140 private:
141 std::ostream& licenses_;
142 bool first_write_ = true;
143};
144
145struct Package {
146 std::string name;
147 std::optional<fs::path> license_file;
148 bool is_root_package;
149};
150
151/// This makes sure trailing slashes on paths are treated the same.
152/// Example:
153/// f("/foo/") == f("/foo") == "foo"
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();
158 }
159 return result;
160}
161
162Package GetPackage(const Data& data,
163 const fs::path& working_dir,
164 const fs::path& relative_path,
165 const LicenseChecker::Flags& flags) {
166 std::string root_package_name = flags.root_package_name.has_value()
167 ? flags.root_package_name.value()
168 : GetDirFilename(working_dir);
169 Package result = {
170 .name = root_package_name,
171 .license_file = FindLicense(data, working_dir, "."),
172 .is_root_package = true,
173 };
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;
183 }
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;
191 }
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;
198 }
199 }
200 if (std::find(kLicenseFileNames.begin(), kLicenseFileNames.end(),
201 relative_path.filename()) != kLicenseFileNames.end()) {
202 result.license_file = working_dir / relative_path;
203 }
204
205 return result;
206}
207
208class LicenseMap {
209 public:
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()) {
216 // License is already seen.
217 comment_set.emplace(std::string(package));
218 }
219 }
220
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);
225 }
226 }
227
228 private:
229 absl::btree_map<std::string, absl::flat_hash_set<std::string>> map_;
230 absl::flat_hash_set<std::string> license_files_;
231};
232
233/// Checks the a license against known licenses and potentially adds it to the
234/// license map.
235/// @param path Path of the license file to check.
236/// @param package Package the license file belongs to.
237/// @param data The Data catalog of known licenses.
238/// @param license_map The LicenseMap tracking seen licenses.
239/// @return OkStatus if the license is known and successfully written to the
240/// catalog.
241absl::Status MatchLicenseFile(const fs::path& path,
242 const Package& package,
243 const Data& data,
244 LicenseMap* license_map) {
245 if (!package.license_file.has_value()) {
246 return absl::InvalidArgumentError("No license file.");
247 }
248 absl::StatusOr<MMapFile> license = MMapFile::Make(path.string());
249 if (!license.ok()) {
250 return license.status();
251 } else {
252 absl::StatusOr<std::vector<Catalog::Match>> matches =
253 data.catalog.FindMatch(
254 std::string_view(license->GetData(), license->GetSize()));
255
256 if (matches.ok()) {
257 for (const Catalog::Match& match : matches.value()) {
258 license_map->Add(package.name, match.GetMatchedText());
259 VLOG(1) << "OK: " << path << " : " << match.GetMatcher();
260 }
261 } else {
262 return absl::NotFoundError(
263 absl::StrCat("Unknown license in ",
264 package.license_file->lexically_normal().string(), " : ",
265 matches.status().message()));
266 }
267 }
268 return absl::OkStatus();
269}
270
271/// State stored across calls to ProcessFile.
272struct ProcessState {
273 LicenseMap license_map;
274 std::vector<absl::Status> errors;
275 absl::flat_hash_set<fs::path> seen_license_files;
276};
277
278namespace {
279bool ProcessSourceCode(const fs::path& relative_path,
280 const MMapFile& file,
281 const Data& data,
282 const Package& package,
283 const LicenseChecker::Flags& flags,
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;
289
290 auto comment_handler = [&](std::string_view comment) -> void {
291 comment_count += 1;
292 VLOG(4) << comment;
293 re2::StringPiece match;
294 if (RE2::PartialMatch(comment, kHeaderLicense, &match)) {
295 if (!VLOG_IS_ON(4)) {
296 VLOG(3) << comment;
297 }
298 absl::StatusOr<std::vector<Catalog::Match>> matches =
299 data.catalog.FindMatch(comment);
300 if (matches.ok()) {
301 did_find_copyright = true;
302 for (const Catalog::Match& match : matches.value()) {
303 license_map->Add(package.name, match.GetMatchedText());
304 VLOG(1) << "OK: " << relative_path.lexically_normal() << " : "
305 << match.GetMatcher();
306 }
307 } else {
309 errors->push_back(absl::NotFoundError(
310 absl::StrCat(relative_path.lexically_normal().string(), " : ",
311 matches.status().message(), "\n", comment)));
312 }
313 VLOG(2) << "NOT_FOUND: " << relative_path.lexically_normal() << " : "
314 << matches.status().message() << "\n"
315 << comment;
316 }
317 }
318 };
319
320 IterateComments(file.GetData(), file.GetSize(), comment_handler);
321
322 // If we didn't find any comments, the input may be a text file, not source
323 // code. So, we attempt to match the full text.
324 if (comment_count <= 0) {
325 comment_handler(std::string_view(file.GetData(), file.GetSize()));
326 }
327
328 return did_find_copyright;
329}
330
331std::vector<std::string_view> SplitLines(std::string_view input) {
332 std::vector<std::string_view> result;
333
334 size_t pos = 0;
335 while (true) {
336 while (pos < input.size() && input[pos] == '\n') {
337 pos++;
338 }
339 const ::std::string::size_type newline = input.find('\n', pos);
340 if (newline == ::std::string::npos) {
341 result.push_back(input.substr(pos));
342 break;
343 } else {
344 result.push_back(input.substr(pos, newline - pos));
345 pos = newline + 1;
346 }
347 }
348
349 return result;
350}
351
352bool ProcessNotices(const fs::path& relative_path,
353 const MMapFile& file,
354 const Data& data,
355 const Package& package,
356 const LicenseChecker::Flags& flags,
357 ProcessState* state) {
358 // std::vector<absl::Status>* errors = &state->errors;
359 // LicenseMap* license_map = &state->license_map;
360 static const std::string kDelimitor =
361 "------------------------------------------------------------------------"
362 "--------";
363 static const std::string pattern_str =
364 "(?s)(.+?)\n\n(.+?)(?:\n?" + kDelimitor + "|$)";
365 static const RE2 regex(pattern_str);
366
367 std::vector<absl::Status>* errors = &state->errors;
368 LicenseMap* license_map = &state->license_map;
369 re2::StringPiece input(file.GetData(), file.GetSize());
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);
374
375 VLOG(4) << license;
376
377 absl::StatusOr<std::vector<Catalog::Match>> matches =
378 data.catalog.FindMatch(license);
379 if (matches.ok()) {
380 for (const Catalog::Match& match : matches.value()) {
381 for (std::string_view project : projects) {
382 license_map->Add(project, match.GetMatchedText());
383 }
384 VLOG(1) << "OK: " << relative_path.lexically_normal() << " : "
385 << match.GetMatcher();
386 }
387 } else {
388 VLOG(2) << "NOT_FOUND: " << relative_path.lexically_normal() << " : "
389 << matches.status().message() << "\n"
390 << license;
392 errors->push_back(absl::NotFoundError(
393 absl::StrCat(relative_path.lexically_normal().string(), " : ",
394 matches.status().message(), "\n", license)));
395 }
396 }
397 }
398 // Not having a license in a NOTICES file isn't technically a problem.
399 return true;
400}
401
402} // namespace
403
404absl::Status ProcessFile(const fs::path& working_dir_path,
405 std::ostream& licenses,
406 const Data& data,
407 const fs::path& full_path,
408 const LicenseChecker::Flags& flags,
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;
414
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();
422 }
423
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());
428 if (is_new_item) {
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));
433 }
434 }
435 } else {
436 VLOG(3) << "No license file: " << relative_path.lexically_normal();
437 }
438
439 absl::StatusOr<MMapFile> file = MMapFile::Make(full_path.string());
440 if (!file.ok()) {
441 if (file.status().code() == absl::StatusCode::kInvalidArgument) {
442 // Zero byte file.
443 return absl::OkStatus();
444 } else {
445 // Failure to mmap file.
446 errors->push_back(file.status());
447 return file.status();
448 }
449 }
450
451 if (full_path.filename().string() == "NOTICES") {
452 did_find_copyright =
453 ProcessNotices(relative_path, *file, data, package, flags, state);
454 } else {
455 did_find_copyright =
456 ProcessSourceCode(relative_path, *file, data, package, flags, state);
457 }
458 if (!did_find_copyright) {
459 if (package.license_file.has_value()) {
460 if (package.is_root_package) {
461 errors->push_back(
462 absl::NotFoundError("Expected root copyright in " +
463 relative_path.lexically_normal().string()));
464 } else {
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()
469 << ")";
470 }
471 } else {
472 errors->push_back(
473 absl::NotFoundError("Expected copyright in " +
474 relative_path.lexically_normal().string()));
475 }
476 }
477 return absl::OkStatus();
478}
479} // namespace
480
481namespace {
482// Searches parent directories for `file_name` starting from `starting_dir`.
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)) {
489 return file_path;
490 }
491 current_dir = current_dir.parent_path();
492 }
493 return fs::path();
494}
495} // namespace
496
497std::vector<absl::Status> LicenseChecker::Run(std::string_view working_dir,
498 std::ostream& licenses,
499 const Data& data) {
500 Flags flags;
501 return Run(working_dir, licenses, data, flags);
502}
503
504std::vector<absl::Status> LicenseChecker::Run(
505 std::string_view working_dir,
506 std::ostream& licenses,
507 const Data& data,
508 const LicenseChecker::Flags& flags) {
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());
512
513 size_t count = 0;
514 ProcessState state;
515
516 // Not every dependency is a git repository, so it won't be considered with
517 // the crawl that happens below of git repositories. For those dependencies
518 // we just crawl the whole directory.
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()) {
523 DepsParser deps_parser;
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)) {
529 // We don't want to process deps that are outside the working
530 // directory.
531 if (dep_path.string().find(working_dir_path.string()) != 0) {
532 continue;
533 }
534
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(),
539 flags, &state);
540 if (!process_result.ok()) {
541 return state.errors;
542 }
543 }
544 }
545 }
546 }
547 }
548 }
549
550 for (const fs::path& git_repo : git_repos) {
551 if (!VLOG_IS_ON(1) && IsStdoutTerminal()) {
552 PrintProgress(count++, git_repos.size());
553 }
554
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());
558 return state.errors;
559 }
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()) {
565 return state.errors;
566 }
567 }
568 }
569
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())));
578 } else {
579 fs::path full_path = data.secondary_dir / entry;
580 Package package =
581 GetPackage(data, working_dir_path, relative_path, flags);
582 absl::StatusOr<MMapFile> file = MMapFile::Make(full_path.string());
583 if (file.ok()) {
584 state.license_map.Add(
585 package.name,
586 std::string_view(file->GetData(), file->GetSize()));
587 } else {
588 state.errors.push_back(file.status());
589 }
590 }
591 }
592 }
593 }
594
595 state.license_map.Write(licenses);
596 if (!VLOG_IS_ON(1) && IsStdoutTerminal()) {
597 PrintProgress(count++, git_repos.size());
598 std::cout << std::endl;
599 }
600
601 return state.errors;
602}
603
604int LicenseChecker::Run(std::string_view working_dir,
605 std::ostream& licenses,
606 std::string_view data_dir,
607 const LicenseChecker::Flags& flags) {
608 absl::StatusOr<Data> data = Data::Open(data_dir);
609 if (!data.ok()) {
610 std::cerr << "Can't load data at " << data_dir << ": " << data.status()
611 << std::endl;
612 return 1;
613 }
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";
618 }
619
620 if (!errors.empty()) {
621 std::cout << "Error count: " << errors.size();
622 }
623
624 return errors.empty() ? 0 : 1;
625}
626
627int LicenseChecker::FileRun(std::string_view working_dir,
628 std::string_view full_path,
629 std::ostream& licenses,
630 std::string_view data_dir,
631 const Flags& flags) {
632 absl::StatusOr<Data> data = Data::Open(data_dir);
633 if (!data.ok()) {
634 std::cerr << "Can't load data at " << data_dir << ": " << data.status()
635 << std::endl;
636 return 1;
637 }
638
639 ProcessState state;
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,
645 flags, &state);
646
647 if (!process_result.ok()) {
648 std::cerr << process_result << std::endl;
649 return 1;
650 }
651
652 state.license_map.Write(licenses);
653 for (const absl::Status& status : state.errors) {
654 std::cerr << status << "\n";
655 }
656
657 if (!state.errors.empty()) {
658 std::cout << "Error count: " << state.errors.size();
659 }
660
661 return state.errors.empty() ? 0 : 1;
662}
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)
A memory mapped file.
Definition mmap_file.h:12
size_t GetSize() const
Definition mmap_file.h:25
static absl::StatusOr< MMapFile > Make(std::string_view path)
Definition mmap_file.cc:13
const char * GetData() const
Definition mmap_file.h:23
static int input(yyscan_t yyscanner)
void IterateComments(const char *buffer, size_t size, std::function< void(std::string_view)> callback)
int32_t value
const char * name
Definition fuchsia.cc:49
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
Definition switch_defs.h:52
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
Definition switch_defs.h:98
Definition data.h:17
static absl::StatusOr< Data > Open(std::string_view data_dir)
Definition data.cc:14
std::optional< std::string > root_package_name
std::shared_ptr< const fml::Mapping > data