diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index bd363d1391b..99a1817fcb0 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -1890,6 +1890,8 @@ FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/dart_component_control FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/dart_component_controller_v2.h FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/dart_runner.cc FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/dart_runner.h +FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.cc +FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.h FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/embedder/builtin.dart FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/embedder/script_runner_snapshot.dart FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/embedder/shim.dart diff --git a/engine/src/flutter/shell/platform/fuchsia/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/BUILD.gn index eeb62a167b7..2565d40461a 100644 --- a/engine/src/flutter/shell/platform/fuchsia/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/BUILD.gn @@ -89,6 +89,9 @@ if (enable_unittests) { group("tests") { testonly = true - deps = [ "flutter:tests" ] + deps = [ + "dart_runner:tests", + "flutter:tests", + ] } } diff --git a/engine/src/flutter/shell/platform/fuchsia/dart_runner/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/dart_runner/BUILD.gn index 3d3fad38283..e9aaa619ec9 100644 --- a/engine/src/flutter/shell/platform/fuchsia/dart_runner/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/dart_runner/BUILD.gn @@ -6,12 +6,85 @@ assert(is_fuchsia) import("//build/fuchsia/sdk.gni") import("//flutter/common/fuchsia_config.gni") +import("//flutter/testing/testing.gni") import("//flutter/tools/fuchsia/dart.gni") import("//flutter/tools/fuchsia/fuchsia_archive.gni") import("//flutter/tools/fuchsia/fuchsia_libs.gni") +template("runner_sources") { + assert(defined(invoker.product), "runner_sources must define product") + + source_set(target_name) { + sources = [ + "builtin_libraries.cc", + "builtin_libraries.h", + "dart_component_controller.cc", + "dart_component_controller.h", + "dart_component_controller_v2.cc", + "dart_component_controller_v2.h", + "dart_runner.cc", + "dart_runner.h", + "dart_test_component_controller_v2.cc", + "dart_test_component_controller_v2.h", + "logging.h", + "service_isolate.cc", + "service_isolate.h", + ] + + dart_public_deps = [] + if (!invoker.product) { + dart_public_deps += [ + "//flutter/shell/platform/fuchsia/runtime/dart/utils:utils", + "//third_party/dart/runtime/bin:dart_io_api", + ] + } else { + dart_public_deps += [ + "//flutter/shell/platform/fuchsia/runtime/dart/utils:utils_product", + "//third_party/dart/runtime/bin:dart_io_api_product", + ] + } + + public_deps = [ + "$fuchsia_sdk_root/pkg:sys_cpp", + "//flutter/fml", + ] + dart_public_deps + + deps = [ + "$fuchsia_sdk_root/fidl:fuchsia.component.runner", + "$fuchsia_sdk_root/fidl:fuchsia.logger", + "$fuchsia_sdk_root/fidl:fuchsia.test", + "$fuchsia_sdk_root/pkg:async", + "$fuchsia_sdk_root/pkg:async-cpp", + "$fuchsia_sdk_root/pkg:async-default", + "$fuchsia_sdk_root/pkg:async-loop", + "$fuchsia_sdk_root/pkg:async-loop-cpp", + "$fuchsia_sdk_root/pkg:async-loop-default", + "$fuchsia_sdk_root/pkg:fidl_cpp", + "$fuchsia_sdk_root/pkg:sys_cpp", + "$fuchsia_sdk_root/pkg:sys_cpp_testing", + "$fuchsia_sdk_root/pkg:sys_inspect_cpp", + "$fuchsia_sdk_root/pkg:syslog", + "$fuchsia_sdk_root/pkg:trace", + "$fuchsia_sdk_root/pkg:vfs_cpp", + "$fuchsia_sdk_root/pkg:zx", + "//flutter/common", + "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", + "//flutter/shell/platform/fuchsia/dart-pkg/zircon", + "//flutter/third_party/tonic", + ] + } +} + +runner_sources("dart_runner_sources") { + product = false +} + +runner_sources("dart_runner_sources_product") { + product = true +} + template("runner") { - assert(defined(invoker.product), "The parameter 'product' must be defined") + assert(defined(invoker.product), "The parameter 'product' must be defined.") assert(defined(invoker.output_name), "The parameter 'output_name' must be defined") @@ -22,60 +95,23 @@ template("runner") { extra_defines += [ "DEBUG" ] # Needed due to direct dart dependencies. } + product_suffix = "" + if (invoker.product) { + product_suffix = "_product" + } + executable(target_name) { output_name = invoker_output_name - sources = [ - "builtin_libraries.cc", - "builtin_libraries.h", - "dart_component_controller.cc", - "dart_component_controller.h", - "dart_component_controller_v2.cc", - "dart_component_controller_v2.h", - "dart_runner.cc", - "dart_runner.h", - "logging.h", - "main.cc", - "service_isolate.cc", - "service_isolate.h", - ] + sources = [ "main.cc" ] defines = extra_defines - dart_deps = [] - if (!invoker.product) { - dart_deps += [ - "//flutter/shell/platform/fuchsia/runtime/dart/utils:utils", - "//third_party/dart/runtime/bin:dart_io_api", - ] - } else { - dart_deps += [ - "//flutter/shell/platform/fuchsia/runtime/dart/utils:utils_product", - "//third_party/dart/runtime/bin:dart_io_api_product", - ] - } - deps = [ - "//flutter/common", - "//flutter/fml", - "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", - "//flutter/shell/platform/fuchsia/dart-pkg/zircon", - "$fuchsia_sdk_root/fidl:fuchsia.component.runner", - "$fuchsia_sdk_root/pkg:async", - "$fuchsia_sdk_root/pkg:async-cpp", - "$fuchsia_sdk_root/pkg:async-default", - "$fuchsia_sdk_root/pkg:async-loop", - "$fuchsia_sdk_root/pkg:async-loop-cpp", - "$fuchsia_sdk_root/pkg:async-loop-default", - "$fuchsia_sdk_root/pkg:fidl_cpp", + ":dart_runner_sources${product_suffix}", "$fuchsia_sdk_root/pkg:sys_inspect_cpp", - "$fuchsia_sdk_root/pkg:sys_cpp", - "$fuchsia_sdk_root/pkg:syslog", - "$fuchsia_sdk_root/pkg:trace", "$fuchsia_sdk_root/pkg:trace-provider-so", - "$fuchsia_sdk_root/pkg:vfs_cpp", - "//flutter/third_party/tonic", - ] + dart_deps + extra_deps + ] + extra_deps } } @@ -273,3 +309,38 @@ jit_runner_package("dart_jit_runner") { jit_runner_package("dart_jit_product_runner") { product = true } + +if (enable_unittests) { + executable("dart_test_runner_unittests") { + testonly = true + + output_name = "dart_runner_tests" + + sources = [ "tests/suite_impl_unittests.cc" ] + + # This is needed for //third_party/googletest for linking zircon symbols. + libs = [ "$fuchsia_sdk_path/arch/$target_cpu/sysroot/lib/libzircon.so" ] + + deps = [ + ":dart_runner_sources", + "//flutter/fml", + "//third_party/dart/runtime:libdart_jit", + "//third_party/dart/runtime/platform:libdart_platform_jit", + "//third_party/googletest:gtest_main", + ] + } + + fuchsia_test_archive("dart_runner_tests") { + deps = [ ":dart_test_runner_unittests" ] + + binary = "$target_name" + } + + # When adding a new dep here, please also ensure the dep is added to + # testing/fuchsia/test_suites.yaml. + group("tests") { + testonly = true + + deps = [ ":dart_runner_tests" ] + } +} diff --git a/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.cc b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.cc index fba5805216c..dfe60d532bd 100644 --- a/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.cc +++ b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.cc @@ -18,6 +18,9 @@ #include "dart_component_controller.h" #include "dart_component_controller_v2.h" +#include "dart_test_component_controller_v2.h" +#include "flutter/fml/command_line.h" +#include "flutter/fml/logging.h" #include "flutter/fml/trace_event.h" #include "logging.h" #include "runtime/dart/utils/inlines.h" @@ -116,7 +119,7 @@ void RunApplicationV1( bool success = app.Setup(); int64_t end = Dart_TimelineGetMicros(); - Dart_TimelineEvent("DartComponentController::Setup", start, end, + Dart_TimelineEvent("DartComponentController::SetUp", start, end, Dart_Timeline_Event_Duration, 0, NULL, NULL); if (success) { app.Run(); @@ -141,7 +144,7 @@ void RunApplicationV2( const bool success = app.SetUp(); const int64_t end = Dart_TimelineGetMicros(); - Dart_TimelineEvent("DartComponentControllerV2::Setup", start, end, + Dart_TimelineEvent("DartComponentControllerV2::SetUp", start, end, Dart_Timeline_Event_Duration, 0, NULL, NULL); if (success) { app.Run(); @@ -152,6 +155,31 @@ void RunApplicationV2( } } +void RunTestApplicationV2( + DartRunner* runner, + fuchsia::component::runner::ComponentStartInfo start_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + controller, + fit::function)> + component_created_callback, + fit::function done_callback) { + const int64_t start = Dart_TimelineGetMicros(); + + auto test_component = std::make_shared( + std::move(start_info), runner_incoming_services, std::move(controller), + std::move(done_callback)); + + component_created_callback(test_component); + + // Start up the dart isolate and serve the suite protocol. + test_component->SetUp(); + + const int64_t end = Dart_TimelineGetMicros(); + Dart_TimelineEvent("DartTestComponentControllerV2::SetUp", start, end, + Dart_Timeline_Event_Duration, 0, NULL, NULL); +} + bool EntropySource(uint8_t* buffer, intptr_t count) { zx_cprng_draw(buffer, count); return true; @@ -159,6 +187,33 @@ bool EntropySource(uint8_t* buffer, intptr_t count) { } // namespace +// "args" are how the component specifies arguments to the runner. +constexpr char kArgsKey[] = "args"; + +/// Parses the |args| field from the "program" field to determine +/// if a test component is being executed. +bool IsTestProgram(const fuchsia::data::Dictionary& program_metadata) { + for (const auto& entry : program_metadata.entries()) { + if (entry.key.compare(kArgsKey) != 0 || entry.value == nullptr) { + continue; + } + auto args = entry.value->str_vec(); + + // fml::CommandLine expects the first argument to be the name of the + // program, so we prepend a dummy argument so we can use fml::CommandLine to + // parse the arguments for us. + std::vector command_line_args = {""}; + command_line_args.insert(command_line_args.end(), args.begin(), args.end()); + fml::CommandLine parsed_args = fml::CommandLineFromIterators( + command_line_args.begin(), command_line_args.end()); + + std::string is_test_str; + return parsed_args.GetOptionValue("is_test", &is_test_str) && + is_test_str == "true"; + } + return false; +} + DartRunner::DartRunner(sys::ComponentContext* context) : context_(context) { context_->outgoing()->AddPublicService( [this](fidl::InterfaceRequest request) { @@ -248,11 +303,34 @@ void DartRunner::Start( fuchsia::component::runner::ComponentStartInfo start_info, fidl::InterfaceRequest controller) { - std::string url_copy = start_info.resolved_url(); - TRACE_EVENT1("dart", "Start", "url", url_copy.c_str()); - std::thread thread(RunApplicationV2, this, std::move(start_info), - context_->svc(), std::move(controller)); - thread.detach(); + // Parse the program field of the component's cml and check if it is a test + // component. If so, serve the |fuchsia.test.Suite| protocol from the + // component's outgoing directory, via DartTestComponentControllerV2. + if (IsTestProgram(start_info.program())) { + std::string url_copy = start_info.resolved_url(); + TRACE_EVENT1("dart", "Start", "url", url_copy.c_str()); + std::thread thread( + RunTestApplicationV2, this, std::move(start_info), context_->svc(), + std::move(controller), + // component_created_callback + [this](std::shared_ptr ptr) { + test_components_.emplace(ptr.get(), std::move(ptr)); + }, + // done_callback + [this](DartTestComponentControllerV2* ptr) { + auto it = test_components_.find(ptr); + if (it != test_components_.end()) { + test_components_.erase(it); + } + }); + thread.detach(); + } else { + std::string url_copy = start_info.resolved_url(); + TRACE_EVENT1("dart", "Start", "url", url_copy.c_str()); + std::thread thread(RunApplicationV2, this, std::move(start_info), + context_->svc(), std::move(controller)); + thread.detach(); + } } } // namespace dart_runner diff --git a/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.h b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.h index ce47eecd817..1d97acf7aec 100644 --- a/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.h +++ b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_runner.h @@ -10,6 +10,7 @@ #include #include +#include "dart_test_component_controller_v2.h" #include "runtime/dart/utils/mapped_resource.h" namespace dart_runner { @@ -33,6 +34,12 @@ class DartRunner : public fuchsia::sys::Runner, fidl::InterfaceRequest controller) override; + // Add test components to this map to ensure it is kept alive in memory for + // the duration of test execution and retrieval of exit code. + std::map> + test_components_; + // Not owned by DartRunner. sys::ComponentContext* context_; fidl::BindingSet bindings_; diff --git a/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.cc b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.cc new file mode 100644 index 00000000000..7d6e6bcfdf4 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.cc @@ -0,0 +1,649 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "dart_test_component_controller_v2.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "runtime/dart/utils/files.h" +#include "runtime/dart/utils/handle_exception.h" +#include "runtime/dart/utils/inlines.h" +#include "runtime/dart/utils/tempfs.h" +#include "third_party/dart/runtime/include/dart_tools_api.h" +#include "third_party/tonic/converter/dart_converter.h" +#include "third_party/tonic/dart_message_handler.h" +#include "third_party/tonic/dart_microtask_queue.h" +#include "third_party/tonic/dart_state.h" +#include "third_party/tonic/logging/dart_error.h" +#include "third_party/tonic/logging/dart_invoke.h" + +#include "builtin_libraries.h" +#include "logging.h" + +using tonic::ToDart; + +namespace dart_runner { + +namespace { + +constexpr char kTmpPath[] = "/tmp"; +constexpr char kTestCaseName[] = "dart_test_v2"; + +constexpr zx::duration kIdleWaitDuration = zx::sec(2); +constexpr zx::duration kIdleNotifyDuration = zx::msec(500); +constexpr zx::duration kIdleSlack = zx::sec(1); + +void AfterTask(async_loop_t*, void*) { + tonic::DartMicrotaskQueue* queue = + tonic::DartMicrotaskQueue::GetForCurrentThread(); + // Verify that the queue exists, as this method could have been called back as + // part of the exit routine, after the destruction of the microtask queue. + if (queue) { + queue->RunMicrotasks(); + } +} + +constexpr async_loop_config_t kLoopConfig = { + .default_accessors = + { + .getter = async_get_default_dispatcher, + .setter = async_set_default_dispatcher, + }, + .make_default_for_current_thread = true, + .epilogue = &AfterTask, +}; + +// Find the last path of the component. +// fuchsia-pkg://fuchsia.com/hello_dart#meta/hello_dart.cmx -> hello_dart.cmx +std::string GetLabelFromUrl(const std::string& url) { + for (size_t i = url.length() - 1; i > 0; i--) { + if (url[i] == '/') { + return url.substr(i + 1, url.length() - 1); + } + } + return url; +} + +// Find the name of the component. +// fuchsia-pkg://fuchsia.com/hello_dart#meta/hello_dart.cm -> hello_dart +std::string GetComponentNameFromUrl(const std::string& url) { + const std::string label = GetLabelFromUrl(url); + for (size_t i = 0; i < label.length(); ++i) { + if (label[i] == '.') { + return label.substr(0, i); + } + } + return label; +} + +} // namespace + +DartTestComponentControllerV2::DartTestComponentControllerV2( + fuchsia::component::runner::ComponentStartInfo start_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + controller, + DoneCallback done_callback) + : loop_(new async::Loop(&kLoopConfig)), + label_(GetLabelFromUrl(start_info.resolved_url())), + url_(std::move(start_info.resolved_url())), + runner_incoming_services_(runner_incoming_services), + start_info_(std::move(start_info)), + binding_(this), + done_callback_(std::move(done_callback)) { + // TODO(fxb/84537): This data path is configured based how we build Flutter + // applications in tree currently, but the way we build the Flutter + // application may change. We should avoid assuming the data path and let the + // CML file specify this data path instead. + const std::string component_name = GetComponentNameFromUrl(url_); + data_path_ = "pkg/data/" + component_name; + + if (controller.is_valid()) { + binding_.Bind(std::move(controller)); + binding_.set_error_handler([this](zx_status_t status) { Kill(); }); + } else { + FX_LOG(ERROR, LOG_TAG, + "Fuchsia component controller endpoint is not valid."); + } + + zx_status_t idle_timer_status = + zx::timer::create(ZX_TIMER_SLACK_LATE, ZX_CLOCK_MONOTONIC, &idle_timer_); + if (idle_timer_status != ZX_OK) { + FX_LOGF(INFO, LOG_TAG, "Idle timer creation failed: %s", + zx_status_get_string(idle_timer_status)); + } else { + idle_wait_.set_object(idle_timer_.get()); + idle_wait_.set_trigger(ZX_TIMER_SIGNALED); + idle_wait_.Begin(async_get_default_dispatcher()); + } +} + +DartTestComponentControllerV2::~DartTestComponentControllerV2() { + if (namespace_) { + fdio_ns_destroy(namespace_); + namespace_ = nullptr; + } + close(stdout_fd_); + close(stderr_fd_); +} + +void DartTestComponentControllerV2::SetUp() { + // Name the thread after the url of the component being launched. + zx::thread::self()->set_property(ZX_PROP_NAME, label_.c_str(), label_.size()); + Dart_SetThreadName(label_.c_str()); + + if (!CreateAndBindNamespace()) { + return; + } + + if (SetUpFromAppSnapshot()) { + FX_LOGF(INFO, LOG_TAG, "%s is running from an app snapshot", url_.c_str()); + } else if (SetUpFromKernel()) { + FX_LOGF(INFO, LOG_TAG, "%s is running from kernel", url_.c_str()); + } else { + FX_LOGF(ERROR, LOG_TAG, "Failed to set up component controller for %s.", + url_.c_str()); + return; + } + + // Serve |fuchsia::test::Suite| on outgoing directory. + suite_context_ = sys::ComponentContext::Create(); + suite_context_->outgoing()->AddPublicService(this->GetHandler()); + suite_context_->outgoing()->Serve( + start_info_.mutable_outgoing_dir()->TakeChannel(), loop_->dispatcher()); + + loop_->Run(); +} + +bool DartTestComponentControllerV2::CreateAndBindNamespace() { + if (!start_info_.has_ns()) { + FX_LOG(ERROR, LOG_TAG, "Component start info does not have a namespace."); + return false; + } + + const zx_status_t ns_create_status = fdio_ns_create(&namespace_); + if (ns_create_status != ZX_OK) { + FX_LOGF(ERROR, LOG_TAG, "Failed to create namespace: %s", + zx_status_get_string(ns_create_status)); + } + + dart_utils::RunnerTemp::SetupComponent(namespace_); + + // Bind each directory in start_info's namespace to the controller's namespace + // instance. + for (auto& ns_entry : *start_info_.mutable_ns()) { + // TODO(akbiggs): Under what circumstances does a namespace entry not + // have a path or directory? Should we log an error for these? + if (!ns_entry.has_path() || !ns_entry.has_directory()) { + continue; + } + + if (ns_entry.path() == kTmpPath) { + // /tmp is covered by the local memfs. + continue; + } + + // We move ownership of the directory & path since RAII is used to keep + // the handle open. + fidl::InterfaceHandle<::fuchsia::io::Directory> dir = + std::move(*ns_entry.mutable_directory()); + const std::string path = std::move(*ns_entry.mutable_path()); + + const zx_status_t ns_bind_status = + fdio_ns_bind(namespace_, path.c_str(), dir.TakeChannel().release()); + if (ns_bind_status != ZX_OK) { + FX_LOGF(ERROR, LOG_TAG, "Failed to bind %s to namespace: %s", + path.c_str(), zx_status_get_string(ns_bind_status)); + return false; + } + } + + return true; +} + +bool DartTestComponentControllerV2::SetUpFromKernel() { + dart_utils::MappedResource manifest; + if (!dart_utils::MappedResource::LoadFromNamespace( + namespace_, data_path_ + "/app.dilplist", manifest)) { + return false; + } + + if (!dart_utils::MappedResource::LoadFromNamespace( + nullptr, "/pkg/data/isolate_core_snapshot_data.bin", + isolate_snapshot_data_)) { + return false; + } + if (!dart_utils::MappedResource::LoadFromNamespace( + nullptr, "/pkg/data/isolate_core_snapshot_instructions.bin", + isolate_snapshot_instructions_, true /* executable */)) { + return false; + } + + if (!CreateIsolate(isolate_snapshot_data_.address(), + isolate_snapshot_instructions_.address())) { + return false; + } + + Dart_EnterScope(); + + std::string str(reinterpret_cast(manifest.address()), + manifest.size()); + Dart_Handle library = Dart_Null(); + for (size_t start = 0; start < manifest.size();) { + size_t end = str.find("\n", start); + if (end == std::string::npos) { + FX_LOG(ERROR, LOG_TAG, "Malformed manifest"); + Dart_ExitScope(); + return false; + } + + std::string path = data_path_ + "/" + str.substr(start, end - start); + start = end + 1; + + dart_utils::MappedResource kernel; + if (!dart_utils::MappedResource::LoadFromNamespace(namespace_, path, + kernel)) { + FX_LOGF(ERROR, LOG_TAG, "Cannot load kernel from namespace: %s", + path.c_str()); + Dart_ExitScope(); + return false; + } + library = Dart_LoadLibraryFromKernel(kernel.address(), kernel.size()); + if (Dart_IsError(library)) { + FX_LOGF(ERROR, LOG_TAG, "Cannot load library from kernel: %s", + Dart_GetError(library)); + Dart_ExitScope(); + return false; + } + + kernel_peices_.emplace_back(std::move(kernel)); + } + Dart_SetRootLibrary(library); + + Dart_Handle result = Dart_FinalizeLoading(false); + if (Dart_IsError(result)) { + FX_LOGF(ERROR, LOG_TAG, "Failed to FinalizeLoading: %s", + Dart_GetError(result)); + Dart_ExitScope(); + return false; + } + + return true; +} + +bool DartTestComponentControllerV2::SetUpFromAppSnapshot() { +#if !defined(AOT_RUNTIME) + return false; +#else + // Load the ELF snapshot as available, and fall back to a blobs snapshot + // otherwise. + const uint8_t *isolate_data, *isolate_instructions; + if (elf_snapshot_.Load(namespace_, data_path_ + "/app_aot_snapshot.so")) { + isolate_data = elf_snapshot_.IsolateData(); + isolate_instructions = elf_snapshot_.IsolateInstrs(); + if (isolate_data == nullptr || isolate_instructions == nullptr) { + return false; + } + } else { + if (!dart_utils::MappedResource::LoadFromNamespace( + namespace_, data_path_ + "/isolate_snapshot_data.bin", + isolate_snapshot_data_)) { + return false; + } + if (!dart_utils::MappedResource::LoadFromNamespace( + namespace_, data_path_ + "/isolate_snapshot_instructions.bin", + isolate_snapshot_instructions_, true /* executable */)) { + return false; + } + } + return CreateIsolate(isolate_data, isolate_instructions); +#endif // defined(AOT_RUNTIME) +} + +bool DartTestComponentControllerV2::CreateIsolate( + const uint8_t* isolate_snapshot_data, + const uint8_t* isolate_snapshot_instructions) { + // Create the isolate from the snapshot. + char* error = nullptr; + + // TODO(dart_runner): Pass if we start using tonic's loader. + intptr_t namespace_fd = -1; + + // Freed in IsolateShutdownCallback. + auto state = new std::shared_ptr(new tonic::DartState( + namespace_fd, [this](Dart_Handle result) { MessageEpilogue(result); })); + + isolate_ = Dart_CreateIsolateGroup( + url_.c_str(), label_.c_str(), isolate_snapshot_data, + isolate_snapshot_instructions, nullptr /* flags */, state, state, &error); + if (!isolate_) { + FX_LOGF(ERROR, LOG_TAG, "Dart_CreateIsolateGroup failed: %s", error); + return false; + } + + state->get()->SetIsolate(isolate_); + + tonic::DartMessageHandler::TaskDispatcher dispatcher = + [loop = loop_.get()](auto callback) { + async::PostTask(loop->dispatcher(), std::move(callback)); + }; + state->get()->message_handler().Initialize(dispatcher); + + state->get()->SetReturnCodeCallback([this](uint32_t return_code) { + return_code_ = return_code; + auto ret_status = return_code == 0 ? fuchsia::test::Status::PASSED + : fuchsia::test::Status::FAILED; + fuchsia::test::Result result; + result.set_status(ret_status); + case_listener_->Finished(std::move(result)); + }); + + return true; +} + +// |fuchsia::test::CaseIterator| +DartTestComponentControllerV2::CaseIterator::CaseIterator( + fidl::InterfaceRequest request, + async_dispatcher_t* dispatcher, + fit::function done_callback) + : binding_(this, std::move(request), dispatcher), + done_callback_(std::move(done_callback)) {} + +// |fuchsia::test::CaseIterator| +void DartTestComponentControllerV2::CaseIterator::GetNext( + GetNextCallback callback) { + // Dart test suites run as multiple tests behind one + // test case. Flip flag once the one test case has been retrieved. + if (first_case_) { + fuchsia::test::Case test_case; + test_case.set_name(std::string(kTestCaseName)); + test_case.set_enabled(true); + std::vector cases; + cases.push_back(std::move(test_case)); + callback(std::move(cases)); + first_case_ = false; + } else { + // Return an pass an empty vector to the callback to indicate there + // are no more tests to be executed. + std::vector cases; + callback(std::move(cases)); + done_callback_(this); + } +} + +// |fuchsia::test::CaseIterator| +std::unique_ptr +DartTestComponentControllerV2::RemoveCaseInterator( + CaseIterator* case_iterator) { + auto it = case_iterators_.find(case_iterator); + std::unique_ptr + case_iterator_ptr; + if (it != case_iterators_.end()) { + case_iterator_ptr = std::move(it->second); + case_iterators_.erase(it); + } + return case_iterator_ptr; +} + +// |fuchsia::test::Suite| +void DartTestComponentControllerV2::GetTests( + fidl::InterfaceRequest iterator) { + auto case_iterator = + std::make_unique(std::move(iterator), loop_->dispatcher(), + [this](CaseIterator* case_iterator) { + RemoveCaseInterator(case_iterator); + }); + case_iterators_.emplace(case_iterator.get(), std::move(case_iterator)); +} + +// |fuchsia::test::Suite| +void DartTestComponentControllerV2::Run( + std::vector tests, + fuchsia::test::RunOptions options, + fidl::InterfaceHandle listener) { + RunDartMain(); + + std::vector args; + if (options.has_arguments()) { + args = std::move(*options.mutable_arguments()); + } + + auto listener_proxy = listener.Bind(); + + for (auto it = tests.begin(); it != tests.end(); it++) { + auto invocation = std::move(*it); + std::string test_case_name; + if (invocation.has_name()) { + test_case_name = invocation.name(); + } + + zx::socket out, err, out_client, err_client; + auto status = zx::socket::create(0, &out, &out_client); + if (status != ZX_OK) { + FML_LOG(FATAL) << "cannot create out socket: " + << zx_status_get_string(status); + } + status = zx::socket::create(0, &err, &err_client); + if (status != ZX_OK) { + FML_LOG(FATAL) << "cannot create error socket: " + << zx_status_get_string(status); + } + + fuchsia::test::StdHandles std_handles; + std_handles.set_err(std::move(err_client)); + std_handles.set_out(std::move(out_client)); + + listener_proxy->OnTestCaseStarted(std::move(invocation), + std::move(std_handles), + case_listener_.NewRequest()); + } + + listener_proxy->OnFinished(); + + if (binding_.is_bound()) { + // From the documentation for ComponentController, ZX_OK should be sent when + // the ComponentController receives a termination request. However, if the + // component exited with a non-zero return code, we indicate this by sending + // an INTERNAL epitaph instead. + // + // TODO(fxb/86666): Communicate return code from the ComponentController + // once v2 has support. + if (return_code_ == 0) { + binding_.Close(ZX_OK); + } else { + binding_.Close(zx_status_t(fuchsia::component::Error::INTERNAL)); + } + } +} + +bool DartTestComponentControllerV2::RunDartMain() { + FML_CHECK(namespace_ != nullptr); + Dart_EnterScope(); + + tonic::DartMicrotaskQueue::StartForCurrentThread(); + + // TODO(fxb/88384): Create a file descriptor for each component that is + // launched and listen for anything that is written to the component. When + // something is written to the component, forward that message along to the + // Fuchsia logger and decorate it with the tag that it came from the + // component. + stdout_fd_ = fileno(stdout); + stderr_fd_ = fileno(stderr); + + fidl::InterfaceRequest outgoing_dir = + std::move(*start_info_.mutable_outgoing_dir()); + InitBuiltinLibrariesForIsolate( + url_, namespace_, stdout_fd_, stderr_fd_, nullptr /* environment */, + outgoing_dir.TakeChannel(), false /* service_isolate */); + + Dart_ExitScope(); + Dart_ExitIsolate(); + char* error = Dart_IsolateMakeRunnable(isolate_); + if (error != nullptr) { + Dart_EnterIsolate(isolate_); + Dart_ShutdownIsolate(); + FX_LOGF(ERROR, LOG_TAG, "Unable to make isolate runnable: %s", error); + free(error); + return false; + } + Dart_EnterIsolate(isolate_); + Dart_EnterScope(); + + // TODO(fxb/88383): Support argument passing. + Dart_Handle corelib = Dart_LookupLibrary(ToDart("dart:core")); + Dart_Handle string_type = + Dart_GetNonNullableType(corelib, ToDart("String"), 0, NULL); + Dart_Handle dart_arguments = + Dart_NewListOfTypeFilled(string_type, Dart_EmptyString(), 0); + + if (Dart_IsError(dart_arguments)) { + FX_LOGF(ERROR, LOG_TAG, "Failed to allocate Dart arguments list: %s", + Dart_GetError(dart_arguments)); + Dart_ExitScope(); + return false; + } + + Dart_Handle user_main = Dart_GetField(Dart_RootLibrary(), ToDart("main")); + + if (Dart_IsError(user_main)) { + FX_LOGF(ERROR, LOG_TAG, + "Failed to locate user_main in the root library: %s", + Dart_GetError(user_main)); + Dart_ExitScope(); + return false; + } + + Dart_Handle fuchsia_lib = Dart_LookupLibrary(tonic::ToDart("dart:fuchsia")); + + if (Dart_IsError(fuchsia_lib)) { + FX_LOGF(ERROR, LOG_TAG, "Failed to locate dart:fuchsia: %s", + Dart_GetError(fuchsia_lib)); + Dart_ExitScope(); + return false; + } + + Dart_Handle main_result = tonic::DartInvokeField( + fuchsia_lib, "_runUserMainForDartRunner", {user_main, dart_arguments}); + + if (Dart_IsError(main_result)) { + auto dart_state = tonic::DartState::Current(); + if (!dart_state->has_set_return_code()) { + // The program hasn't set a return code meaning this exit is unexpected. + FX_LOG(ERROR, LOG_TAG, Dart_GetError(main_result)); + return_code_ = tonic::GetErrorExitCode(main_result); + + dart_utils::HandleIfException(runner_incoming_services_, url_, + main_result); + } + Dart_ExitScope(); + return false; + } + Dart_ExitScope(); + return true; +} + +void DartTestComponentControllerV2::Kill() { + done_callback_(this); + suite_bindings_.CloseAll(); + if (Dart_CurrentIsolate()) { + tonic::DartMicrotaskQueue* queue = + tonic::DartMicrotaskQueue::GetForCurrentThread(); + if (queue) { + queue->Destroy(); + } + + loop_->Quit(); + + // TODO(rosswang): The docs warn of threading issues if doing this again, + // but without this, attempting to shut down the isolate finalizes app + // contexts that can't tell a shutdown is in progress and so fatal. + Dart_SetMessageNotifyCallback(nullptr); + + Dart_ShutdownIsolate(); + } +} + +void DartTestComponentControllerV2::MessageEpilogue(Dart_Handle result) { + auto dart_state = tonic::DartState::Current(); + // If the Dart program has set a return code, then it is intending to shut + // down by way of a fatal error, and so there is no need to override + // return_code_. + if (dart_state->has_set_return_code()) { + Dart_ShutdownIsolate(); + return; + } + + dart_utils::HandleIfException(runner_incoming_services_, url_, result); + + // Otherwise, see if there was any other error. + return_code_ = tonic::GetErrorExitCode(result); + if (return_code_ != 0) { + Dart_ShutdownIsolate(); + return; + } + + idle_start_ = zx::clock::get_monotonic(); + zx_status_t status = + idle_timer_.set(idle_start_ + kIdleWaitDuration, kIdleSlack); + if (status != ZX_OK) { + FX_LOGF(INFO, LOG_TAG, "Idle timer set failed: %s", + zx_status_get_string(status)); + } +} + +void DartTestComponentControllerV2::Stop() { + Kill(); +} + +void DartTestComponentControllerV2::OnIdleTimer( + async_dispatcher_t* dispatcher, + async::WaitBase* wait, + zx_status_t status, + const zx_packet_signal* signal) { + if ((status != ZX_OK) || !(signal->observed & ZX_TIMER_SIGNALED) || + !Dart_CurrentIsolate()) { + // Timer closed or isolate shutdown. + return; + } + + zx::time deadline = idle_start_ + kIdleWaitDuration; + zx::time now = zx::clock::get_monotonic(); + if (now >= deadline) { + // No Dart message has been processed for kIdleWaitDuration: assume we'll + // stay idle for kIdleNotifyDuration. + Dart_NotifyIdle((now + kIdleNotifyDuration).get()); + idle_start_ = zx::time(0); + idle_timer_.cancel(); // De-assert signal. + } else { + // Early wakeup or message pushed idle time forward: reschedule. + zx_status_t status = idle_timer_.set(deadline, kIdleSlack); + if (status != ZX_OK) { + FX_LOGF(INFO, LOG_TAG, "Idle timer set failed: %s", + zx_status_get_string(status)); + } + } + wait->Begin(dispatcher); // ignore errors +} + +} // namespace dart_runner diff --git a/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.h b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.h new file mode 100644 index 00000000000..f16ed2c5d63 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/dart_runner/dart_test_component_controller_v2.h @@ -0,0 +1,163 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_DART_RUNNER_DART_TEST_COMPONENT_CONTROLLER_V2_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_DART_RUNNER_DART_TEST_COMPONENT_CONTROLLER_V2_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "lib/fidl/cpp/binding.h" +#include "runtime/dart/utils/mapped_resource.h" +#include "third_party/dart/runtime/include/dart_api.h" + +namespace dart_runner { + +/// Starts a Dart test component written in CFv2. It's different from +/// DartComponentControllerV2 in that it must implement the +/// |fuchsia.test.Suite| protocol. It was forked to avoid a naming clash +/// between the two classes' methods as the Suite protocol requires a Run() +/// method for the test_manager to call on. This way, we avoid an extra layer +/// between the test_manager and actual test execution. +/// TODO(fxb/98369): Look into combining the two component classes once dart +/// testing is stable. +class DartTestComponentControllerV2 + : public fuchsia::component::runner::ComponentController, + public fuchsia::test::Suite { + using DoneCallback = fit::function; + + public: + DartTestComponentControllerV2( + fuchsia::component::runner::ComponentStartInfo start_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + controller, + DoneCallback done_callback); + + ~DartTestComponentControllerV2() override; + + /// Sets up the controller. + /// + /// This should be called before |Run|. + void SetUp(); + + /// |Suite| protocol implementation. + void GetTests( + fidl::InterfaceRequest iterator) override; + + /// |Suite| protocol implementation. + void Run(std::vector tests, + fuchsia::test::RunOptions options, + fidl::InterfaceHandle listener) override; + + fidl::InterfaceRequestHandler GetHandler() { + return suite_bindings_.GetHandler(this, loop_->dispatcher()); + } + + private: + /// Helper for actually running the Dart main. Returns true if successful, + /// false otherwise. + bool RunDartMain(); + + /// Creates and binds the namespace for this component. Returns true if + /// successful, false otherwise. + bool CreateAndBindNamespace(); + + bool SetUpFromKernel(); + bool SetUpFromAppSnapshot(); + + bool CreateIsolate(const uint8_t* isolate_snapshot_data, + const uint8_t* isolate_snapshot_instructions); + + // |ComponentController| + void Kill() override; + void Stop() override; + + // Idle notification. + void MessageEpilogue(Dart_Handle result); + void OnIdleTimer(async_dispatcher_t* dispatcher, + async::WaitBase* wait, + zx_status_t status, + const zx_packet_signal* signal); + + // |CaseIterator| + class CaseIterator final : public fuchsia::test::CaseIterator { + public: + CaseIterator(fidl::InterfaceRequest request, + async_dispatcher_t* dispatcher, + fit::function done_callback); + + void GetNext(GetNextCallback callback) override; + + private: + bool first_case_ = true; + fidl::Binding binding_; + fit::function done_callback_; + }; + + std::unique_ptr RemoveCaseInterator(CaseIterator*); + + // We only need one case_listener currently as dart tests are run as one + // large test file. In future iterations, case_listeners must be + // created per test case. + fidl::InterfacePtr case_listener_; + std::map> case_iterators_; + + // |Suite| + + /// Exposes suite protocol on behalf of test component. + std::unique_ptr suite_context_; + fidl::BindingSet suite_bindings_; + + // The loop must be the first declared member so that it gets destroyed after + // binding_ which expects the existence of a loop. + std::unique_ptr loop_; + + std::string label_; + std::string url_; + std::shared_ptr runner_incoming_services_; + std::string data_path_; + std::unique_ptr context_; + + fuchsia::component::runner::ComponentStartInfo start_info_; + fidl::Binding binding_; + DoneCallback done_callback_; + + fdio_ns_t* namespace_ = nullptr; + int stdout_fd_ = -1; + int stderr_fd_ = -1; + + dart_utils::ElfSnapshot elf_snapshot_; // AOT snapshot + dart_utils::MappedResource isolate_snapshot_data_; // JIT snapshot + dart_utils::MappedResource isolate_snapshot_instructions_; // JIT snapshot + std::vector kernel_peices_; + + Dart_Isolate isolate_; + int32_t return_code_ = 0; + + zx::time idle_start_{0}; + zx::timer idle_timer_; + async::WaitMethod + idle_wait_{this}; + + // Disallow copy and assignment. + DartTestComponentControllerV2(const DartTestComponentControllerV2&) = delete; + DartTestComponentControllerV2& operator=( + const DartTestComponentControllerV2&) = delete; +}; + +} // namespace dart_runner + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_DART_RUNNER_DART_TEST_COMPONENT_CONTROLLER_V2_H_ diff --git a/engine/src/flutter/shell/platform/fuchsia/dart_runner/meta/dart_jit_runner.cml b/engine/src/flutter/shell/platform/fuchsia/dart_runner/meta/dart_jit_runner.cml index 2552f7ea676..545ed8091dc 100644 --- a/engine/src/flutter/shell/platform/fuchsia/dart_runner/meta/dart_jit_runner.cml +++ b/engine/src/flutter/shell/platform/fuchsia/dart_runner/meta/dart_jit_runner.cml @@ -17,6 +17,14 @@ path: "/svc/fuchsia.component.runner.ComponentRunner", }, ], + use: [ + { + protocol: [ + "fuchsia.sys.Environment", + "fuchsia.sys.Loader", + ], + }, + ], expose: [ { runner: "dart_jit_runner", diff --git a/engine/src/flutter/shell/platform/fuchsia/dart_runner/tests/suite_impl_unittests.cc b/engine/src/flutter/shell/platform/fuchsia/dart_runner/tests/suite_impl_unittests.cc new file mode 100644 index 00000000000..485dfd24ded --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/dart_runner/tests/suite_impl_unittests.cc @@ -0,0 +1,23 @@ +// Copyright 2022 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "dart_runner/dart_test_component_controller_v2.h" + +#include "gtest/gtest.h" + +namespace dart_runner::testing { +namespace { + +std::string GetCurrentTestName() { + return ::testing::UnitTest::GetInstance()->current_test_info()->name(); +} + +} // namespace + +// TODO(naudzghebre): Add unit tests for the dart_test_runner. +TEST(SuiteImplTest, EQUALITY) { + EXPECT_EQ(1, 1) << GetCurrentTestName(); +} + +} // namespace dart_runner::testing diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn index 3cba1c486c3..3ddd06b9327 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn @@ -811,7 +811,7 @@ if (enable_unittests) { } # When adding a new dep here, please also ensure the dep is added to - # testing/fuchsia/run_tests.sh and testing/fuchsia/test_fars + # testing/fuchsia/test_suites.yaml. group("tests") { testonly = true diff --git a/engine/src/flutter/sky/packages/sky_engine/LICENSE b/engine/src/flutter/sky/packages/sky_engine/LICENSE index fb6e9f2b5c6..8e727def4e0 100644 --- a/engine/src/flutter/sky/packages/sky_engine/LICENSE +++ b/engine/src/flutter/sky/packages/sky_engine/LICENSE @@ -5324,6 +5324,34 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- +engine + +Copyright 2022 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- expat Copyright (c) 1997-2000 Thai Open Source Software Center Ltd diff --git a/engine/src/flutter/testing/fuchsia/test_suites.yaml b/engine/src/flutter/testing/fuchsia/test_suites.yaml index 95d0a9e8fd9..b99a3fa52b9 100644 --- a/engine/src/flutter/testing/fuchsia/test_suites.yaml +++ b/engine/src/flutter/testing/fuchsia/test_suites.yaml @@ -12,6 +12,8 @@ - flutter_jit_runner-0.far # v2 components. +- test_command: run-test-suite fuchsia-pkg://fuchsia.com/dart_runner_tests#meta/dart_runner_tests.cm + package: dart_runner_tests-0.far - test_command: run-test-suite fuchsia-pkg://fuchsia.com/flutter_runner_tzdata_tests#meta/flutter_runner_tzdata_tests.cm package: flutter_runner_tzdata_tests-0.far - test_command: run-test-suite fuchsia-pkg://fuchsia.com/fml_tests#meta/fml_tests.cm -- --gtest_filter=-MessageLoop.TimeSensitiveTest_*:FileTest.CanTruncateAndWrite:FileTest.CreateDirectoryStructure diff --git a/engine/src/flutter/tools/fuchsia/all_packages.list b/engine/src/flutter/tools/fuchsia/all_packages.list index 94e2a96700c..cbfe45a1cb4 100644 --- a/engine/src/flutter/tools/fuchsia/all_packages.list +++ b/engine/src/flutter/tools/fuchsia/all_packages.list @@ -1,5 +1,6 @@ dart_aot_runner_package_manifest.json dart_jit_runner_package_manifest.json +dart_runner_tests_package_manifest.json embedder_tests_package_manifest.json flow_tests_package_manifest.json flutter_aot_runner_package_manifest.json diff --git a/engine/src/flutter/tools/fuchsia/fuchsia_archive.gni b/engine/src/flutter/tools/fuchsia/fuchsia_archive.gni index 6d75ed18c0c..59ea8da46ba 100644 --- a/engine/src/flutter/tools/fuchsia/fuchsia_archive.gni +++ b/engine/src/flutter/tools/fuchsia/fuchsia_archive.gni @@ -153,6 +153,7 @@ template("_fuchsia_archive") { ":${target_name}_dir", ":${_dbg_symbols_target}", ] + sources = copy_outputs inputs = [] diff --git a/engine/src/flutter/tools/fuchsia/gen_package.py b/engine/src/flutter/tools/fuchsia/gen_package.py index 69e7f8e290d..5ebc4ee555a 100755 --- a/engine/src/flutter/tools/fuchsia/gen_package.py +++ b/engine/src/flutter/tools/fuchsia/gen_package.py @@ -70,8 +70,8 @@ def main(): assert os.path.exists(args.pm_bin) assert os.path.exists(args.package_dir) - pkg_dir = args.package_dir + if not os.path.exists(os.path.join(pkg_dir, 'meta', 'package')): CreateMetaPackage(pkg_dir, args.far_name)