Unverified Commit 8abf0a6d authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Switch to CMake for Linux desktop (#57238)

Updates the Linux templates to use CMake+ninja, rather than Make, and updates the tooling to generate CMake support files rather than Make support files, and to drive the build using cmake and ninja.

Also updates doctor to check for cmake and ninja in place of make.

Note: While we could use CMake+Make rather than CMake+ninja, in testing ninja handled the tool_backend.sh call much better, calling it only once rather than once per dependent target. While it does add another dependency that people are less likely to already have, it's widely available in package managers, as well as being available as a direct download. Longer term, we could potentially switch from ninja to Make if it's an issue.

Fixes #52751
parent 2b2bbfa6
......@@ -213,6 +213,21 @@ class UserMessages {
String get visualStudioIsIncomplete => 'The current Visual Studio installation is incomplete. Please reinstall Visual Studio.';
String get visualStudioRebootRequired => 'Visual Studio requires a reboot of your system to complete installation.';
// Messages used in LinuxDoctorValidator
String get clangMissing => 'clang++ is required for Linux development.\n'
'It is likely available from your distribution (e.g.: apt install clang), or '
'can be downloaded from https://releases.llvm.org/';
String clangTooOld(String minimumVersion) => 'clang++ $minimumVersion or later is required.';
String get cmakeMissing => 'CMake is required for Linux development.\n'
'It is likely available from your distribution (e.g.: apt install cmake), or '
'can be downloaded from https://cmake.org/download/';
String cmakeTooOld(String minimumVersion) => 'cmake $minimumVersion or later is required.';
String ninjaVersion(String version) => 'ninja version $version';
String get ninjaMissing => 'ninja is required for Linux development.\n'
'It is likely available from your distribution (e.g.: apt install ninja-build), or '
'can be downloaded from https://github.com/ninja-build/ninja/releases';
String ninjaTooOld(String minimumVersion) => 'ninja $minimumVersion or later is required.';
// Messages used in FlutterCommand
String flutterElapsedTime(String name, String elapsedTime) => '"flutter $name" took $elapsedTime.';
String get flutterNoDevelopmentDevice =>
......
......@@ -97,6 +97,7 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
if (linuxWorkflow.appliesToHostPlatform)
LinuxDoctorValidator(
processManager: globals.processManager,
userMessages: userMessages,
),
if (windowsWorkflow.appliesToHostPlatform)
visualStudioValidator,
......
......@@ -9,7 +9,7 @@ import '../base/file_system.dart';
import '../build_info.dart';
import '../globals.dart' as globals;
import '../project.dart';
import 'makefile.dart';
import 'cmake.dart';
abstract class LinuxApp extends ApplicationPackage {
LinuxApp({@required String projectBundleId}) : super(id: projectBundleId);
......@@ -58,8 +58,13 @@ class BuildableLinuxApp extends LinuxApp {
@override
String executable(BuildMode buildMode) {
final String binaryName = makefileExecutableName(project);
return globals.fs.path.join(getLinuxBuildDirectory(), getNameForBuildMode(buildMode), binaryName);
final String binaryName = getCmakeExecutableName(project);
return globals.fs.path.join(
getLinuxBuildDirectory(),
getNameForBuildMode(buildMode),
'bundle',
binaryName,
);
}
@override
......
......@@ -7,11 +7,13 @@ import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../plugins.dart';
import '../project.dart';
import 'cmake.dart';
/// Builds the Linux project through the Makefile.
Future<void> buildLinux(
......@@ -19,7 +21,7 @@ Future<void> buildLinux(
BuildInfo buildInfo, {
String target = 'lib/main.dart',
}) async {
if (!linuxProject.makeFile.existsSync()) {
if (!linuxProject.cmakeFile.existsSync()) {
throwToolExit('No Linux desktop project configured. See '
'https://github.com/flutter/flutter/wiki/Desktop-shells#create '
'to learn about adding Linux support to a project.');
......@@ -39,29 +41,18 @@ Future<void> buildLinux(
'Upgrade Flutter and try again.');
}
final StringBuffer buffer = StringBuffer('''
# Generated code do not commit.
export FLUTTER_ROOT=${Cache.flutterRoot}
export FLUTTER_TARGET=$target
export PROJECT_DIR=${linuxProject.project.directory.path}
''');
// Build the environment that needs to be set for the re-entrant flutter build
// step.
final Map<String, String> environmentConfig = buildInfo.toEnvironmentConfig();
for (final String key in environmentConfig.keys) {
final String value = environmentConfig[key];
buffer.writeln('export $key=$value');
}
environmentConfig['FLUTTER_TARGET'] = target;
if (globals.artifacts is LocalEngineArtifacts) {
final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
final String engineOutPath = localEngineArtifacts.engineOutPath;
buffer.writeln('export FLUTTER_ENGINE=${globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath))}');
buffer.writeln('export LOCAL_ENGINE=${globals.fs.path.basename(engineOutPath)}');
environmentConfig['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath));
environmentConfig['LOCAL_ENGINE'] = globals.fs.path.basename(engineOutPath);
}
writeGeneratedCmakeConfig(Cache.flutterRoot, linuxProject, environmentConfig);
/// Cache flutter configuration files in the linux directory.
linuxProject.generatedMakeConfigFile
..createSync(recursive: true)
..writeAsStringSync(buffer.toString());
createPluginSymlinks(linuxProject.project);
if (!buildInfo.isDebug) {
......@@ -73,36 +64,77 @@ export PROJECT_DIR=${linuxProject.project.directory.path}
globals.printStatus('');
}
// Invoke make.
final String buildFlag = getNameForBuildMode(buildInfo.mode ?? BuildMode.release);
final Stopwatch sw = Stopwatch()..start();
final Status status = globals.logger.startProgress(
'Building Linux application...',
timeout: null,
);
try {
final String buildModeName = getNameForBuildMode(buildInfo.mode ?? BuildMode.release);
final Directory buildDirectory = globals.fs.directory(getLinuxBuildDirectory()).childDirectory(buildModeName);
await _runCmake(buildModeName, linuxProject.cmakeFile.parent, buildDirectory);
await _runBuild(buildDirectory);
} finally {
status.cancel();
}
}
Future<void> _runCmake(String buildModeName, Directory sourceDir, Directory buildDir) async {
final Stopwatch sw = Stopwatch()..start();
final String buildFlag = toTitleCase(buildModeName);
int result;
try {
result = await processUtils.stream(
<String>[
'cmake',
'-S',
sourceDir.path,
'-B',
buildDir.path,
'-G',
'Ninja',
'-DCMAKE_BUILD_TYPE=$buildFlag',
],
environment: <String, String>{
'CC': 'clang',
'CXX': 'clang++'
},
trace: true,
);
} on ArgumentError {
throwToolExit("cmake not found. Run 'flutter doctor' for more information.");
}
if (result != 0) {
throwToolExit('Unable to generate build files');
}
globals.flutterUsage.sendTiming('build', 'cmake-linux', Duration(milliseconds: sw.elapsedMilliseconds));
}
Future<void> _runBuild(Directory buildDir) async {
final Stopwatch sw = Stopwatch()..start();
int result;
try {
result = await processUtils.stream(
<String>[
'make',
'ninja',
'-C',
linuxProject.makeFile.parent.path,
'BUILD=$buildFlag',
buildDir.path,
'install',
],
environment: <String, String>{
if (globals.logger.isVerbose)
'VERBOSE_SCRIPT_LOGGING': 'true'
}, trace: true,
},
trace: true,
);
} on ArgumentError {
throwToolExit("make not found. Run 'flutter doctor' for more information.");
} finally {
status.cancel();
throwToolExit("ninja not found. Run 'flutter doctor' for more information.");
}
if (result != 0) {
throwToolExit('Build process failed');
}
globals.flutterUsage.sendTiming('build', 'make-linux', Duration(milliseconds: sw.elapsedMilliseconds));
globals.flutterUsage.sendTiming('build', 'linux-ninja', Duration(milliseconds: sw.elapsedMilliseconds));
}
// Checks the template version of [project] against the current template
......
// Copyright 2014 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.
import '../project.dart';
/// Extracts the `BINARY_NAME` from a Linux project CMake file.
///
/// Returns `null` if it cannot be found.
String getCmakeExecutableName(LinuxProject project) {
if (!project.cmakeFile.existsSync()) {
return null;
}
final RegExp nameSetPattern = RegExp(r'^\s*set\(BINARY_NAME\s*"(.*)"\s*\)\s*$');
for (final String line in project.cmakeFile.readAsLinesSync()) {
final RegExpMatch match = nameSetPattern.firstMatch(line);
if (match != null) {
return match.group(1);
}
}
return null;
}
/// Writes a generated CMake configuration file for [project], including
/// variables expected by the build template and an environment variable list
/// for calling back into Flutter.
void writeGeneratedCmakeConfig(String flutterRoot, LinuxProject project, Map<String, String> environment) {
// Only a limited set of variables are needed by the CMake files themselves,
// the rest are put into a list to pass to the re-entrant build step.
final StringBuffer buffer = StringBuffer('''
# Generated code do not commit.
set(FLUTTER_ROOT "$flutterRoot")
set(PROJECT_DIR "${project.project.directory.path}")
# Environment variables to pass to tool_backend.sh
list(APPEND FLUTTER_TOOL_ENVIRONMENT
"FLUTTER_ROOT=\\"\${FLUTTER_ROOT}\\""
"PROJECT_DIR=\\"\${PROJECT_DIR}\\""
''');
for (final String key in environment.keys) {
final String value = environment[key];
buffer.writeln(' "$key=\\"$value\\""');
}
buffer.writeln(')');
project.generatedCmakeConfigFile
..createSync(recursive: true)
..writeAsStringSync(buffer.toString());
}
......@@ -6,70 +6,133 @@ import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/io.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../doctor.dart';
/// A combination of version description and parsed version number.
class _VersionInfo {
/// Constructs a VersionInfo from a version description string.
///
/// This should contain a version number. For example:
/// "clang version 9.0.1-6+build1"
_VersionInfo(this.description) {
final String versionString = RegExp(r'[0-9]+\.[0-9]+\.[0-9]+').firstMatch(description).group(0);
number = Version.parse(versionString);
}
// The full info string reported by the binary.
String description;
// The parsed Version.
Version number;
}
/// A validator that checks for Clang and Make build dependencies
class LinuxDoctorValidator extends DoctorValidator {
LinuxDoctorValidator({
@required ProcessManager processManager,
@required UserMessages userMessages,
}) : _processManager = processManager,
_userMessages = userMessages,
super('Linux toolchain - develop for Linux desktop');
final ProcessManager _processManager;
final UserMessages _userMessages;
static const String kClangBinary = 'clang++';
static const String kCmakeBinary = 'cmake';
static const String kNinjaBinary = 'ninja';
/// The minimum version of clang supported.
final Version minimumClangVersion = Version(3, 4, 0);
final Map<String, Version> _requiredBinaryVersions = <String, Version>{
kClangBinary: Version(3, 4, 0),
kCmakeBinary: Version(3, 10, 0),
kNinjaBinary: Version(1, 8, 0),
};
@override
Future<ValidationResult> validate() async {
ValidationType validationType = ValidationType.installed;
final List<ValidationMessage> messages = <ValidationMessage>[];
/// Check for a minimum version of Clang.
ProcessResult clangResult;
try {
clangResult = await _processManager.run(const <String>[
'clang++',
'--version',
]);
} on ArgumentError {
// ignore error.
}
if (clangResult == null || clangResult.exitCode != 0) {
final Map<String, _VersionInfo> installedVersions = <String, _VersionInfo>{
// Sort the check to make the call order predictable for unit tests.
for (String binary in _requiredBinaryVersions.keys.toList()..sort())
binary: await _getBinaryVersion(binary)
};
// Determine overall validation level.
if (installedVersions.values.contains(null)) {
validationType = ValidationType.missing;
messages.add(const ValidationMessage.error('clang++ is not installed'));
} else {
final String firstLine = (clangResult.stdout as String).split('\n').first.trim();
final String versionString = RegExp(r'[0-9]+\.[0-9]+\.[0-9]+').firstMatch(firstLine).group(0);
final Version version = Version.parse(versionString);
if (version >= minimumClangVersion) {
messages.add(ValidationMessage('clang++ $version'));
} else if (installedVersions.keys.any((String binary) =>
installedVersions[binary].number < _requiredBinaryVersions[binary])) {
validationType = ValidationType.partial;
}
// Message for Clang.
{
final _VersionInfo version = installedVersions[kClangBinary];
if (version == null) {
messages.add(ValidationMessage.error(_userMessages.clangMissing));
} else {
messages.add(ValidationMessage(version.description));
final Version requiredVersion = _requiredBinaryVersions[kClangBinary];
if (version.number < requiredVersion) {
messages.add(ValidationMessage.error(_userMessages.clangTooOld(requiredVersion.toString())));
}
}
}
// Message for CMake.
{
final _VersionInfo version = installedVersions[kCmakeBinary];
if (version == null) {
messages.add(ValidationMessage.error(_userMessages.cmakeMissing));
} else {
validationType = ValidationType.partial;
messages.add(ValidationMessage.error('clang++ $version is below minimum version of $minimumClangVersion'));
messages.add(ValidationMessage(version.description));
final Version requiredVersion = _requiredBinaryVersions[kCmakeBinary];
if (version.number < requiredVersion) {
messages.add(ValidationMessage.error(_userMessages.cmakeTooOld(requiredVersion.toString())));
}
}
}
/// Check for make.
// TODO(jonahwilliams): tighten this check to include a version when we have
// a better idea about what is supported.
ProcessResult makeResult;
// Message for ninja.
{
final _VersionInfo version = installedVersions[kNinjaBinary];
if (version == null) {
messages.add(ValidationMessage.error(_userMessages.ninjaMissing));
} else {
// The full version description is just the number, so context.
messages.add(ValidationMessage(_userMessages.ninjaVersion(version.description)));
final Version requiredVersion = _requiredBinaryVersions[kNinjaBinary];
if (version.number < requiredVersion) {
messages.add(ValidationMessage.error(_userMessages.ninjaTooOld(requiredVersion.toString())));
}
}
}
return ValidationResult(validationType, messages);
}
/// Returns the installed version of [binary], or null if it's not installed.
///
/// Requires tha [binary] take a '--version' flag, and print a version of the
/// form x.y.z somewhere on the first line of output.
Future<_VersionInfo> _getBinaryVersion(String binary) async {
ProcessResult result;
try {
makeResult = await _processManager.run(const <String>[
'make',
result = await _processManager.run(<String>[
binary,
'--version',
]);
} on ArgumentError {
// ignore error.
}
if (makeResult == null || makeResult.exitCode != 0) {
validationType = ValidationType.missing;
messages.add(const ValidationMessage.error('make is not installed'));
} else {
final String firstLine = (makeResult.stdout as String).split('\n').first.trim();
messages.add(ValidationMessage(firstLine));
if (result == null || result.exitCode != 0) {
return null;
}
return ValidationResult(validationType, messages);
final String firstLine = (result.stdout as String).split('\n').first.trim();
return _VersionInfo(firstLine);
}
}
// Copyright 2014 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.
import '../base/file_system.dart';
import '../project.dart';
// The setting that controls the executable name in the linux makefile.
const String _kBinaryNameVariable = 'BINARY_NAME=';
/// Extracts the `BINARY_NAME` from a linux project Makefile.
///
/// Returns `null` if it cannot be found.
String makefileExecutableName(LinuxProject project) {
// Support the binary name being set either in the Makefile, or in the
// separate configution include file used by the template.
final List<File> makeFiles = <File>[
project.makeFile.parent.childFile('app_configuration.mk'),
project.makeFile,
];
for (final File file in makeFiles) {
if (!file.existsSync()) {
continue;
}
for (final String line in file.readAsLinesSync()) {
if (line.startsWith(_kBinaryNameVariable)) {
return line.split(_kBinaryNameVariable).last.trim();
}
}
}
return null;
}
......@@ -794,38 +794,17 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
}
''';
const String _linuxPluginMakefileTemplate = '''
# Plugins to include in the build.
GENERATED_PLUGINS=\\
const String _linuxPluginCmakefileTemplate = r'''
list(APPEND FLUTTER_PLUGIN_LIST
{{#plugins}}
\t{{name}} \\
{{name}}
{{/plugins}}
)
GENERATED_PLUGINS_DIR={{pluginsDir}}
# A plugin library name plugin name with _plugin appended.
GENERATED_PLUGIN_LIB_NAMES=\$(foreach plugin,\$(GENERATED_PLUGINS),\$(plugin)_plugin)
# Variables for use in the enclosing Makefile. Changes to these names are
# breaking changes.
PLUGIN_TARGETS=\$(GENERATED_PLUGINS)
PLUGIN_LIBRARIES=\$(foreach plugin,\$(GENERATED_PLUGIN_LIB_NAMES),\\
\t\$(OUT_DIR)/lib\$(plugin).so)
PLUGIN_LDFLAGS=\$(patsubst %,-l%,\$(GENERATED_PLUGIN_LIB_NAMES))
PLUGIN_CPPFLAGS=\$(foreach plugin,\$(GENERATED_PLUGINS),\\
\t-I\$(GENERATED_PLUGINS_DIR)/\$(plugin)/linux)
# Targets
# Implicit rules don't match phony targets, so list plugin builds explicitly.
{{#plugins}}
\$(OUT_DIR)/lib{{name}}_plugin.so: | {{name}}
{{/plugins}}
.PHONY: \$(GENERATED_PLUGINS)
\$(GENERATED_PLUGINS):
make -C \$(GENERATED_PLUGINS_DIR)/\$@/linux \\
OUT_DIR=\$(OUT_DIR) \\
FLUTTER_EPHEMERAL_DIR="\$(abspath {{ephemeralDir}})"
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory({{pluginsDir}}/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
endforeach(plugin)
''';
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
......@@ -871,31 +850,26 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
Future<void> _writeLinuxPluginFiles(FlutterProject project, List<Plugin> plugins) async {
final List<Plugin>nativePlugins = _filterNativePlugins(plugins, LinuxPlugin.kConfigKey);
final List<Map<String, dynamic>> linuxPlugins = _extractPlatformMaps(nativePlugins, LinuxPlugin.kConfigKey);
// The generated makefile is checked in, so can't use absolute paths. It is
// included by the main makefile, so relative paths must be relative to that
// file's directory.
final String makefileDirPath = project.linux.makeFile.parent.absolute.path;
// The generated file is checked in, so can't use absolute paths. It is
// included by the main CMakeLists.txt, so relative paths must be relative to
// that file's directory.
final String makefileDirPath = project.linux.cmakeFile.parent.absolute.path;
final Map<String, dynamic> context = <String, dynamic>{
'plugins': linuxPlugins,
'ephemeralDir': globals.fs.path.relative(
project.linux.ephemeralDirectory.absolute.path,
from: makefileDirPath,
),
'pluginsDir': globals.fs.path.relative(
project.linux.pluginSymlinkDirectory.absolute.path,
from: makefileDirPath,
),
};
await _writeCppPluginRegistrant(project.linux.managedDirectory, context);
await _writeLinuxPluginMakefile(project.linux.managedDirectory, context);
await _writeLinuxPluginCmakefile(project.linux.generatedPluginCmakeFile, context);
}
Future<void> _writeLinuxPluginMakefile(Directory destination, Map<String, dynamic> templateContext) async {
final String registryDirectory = destination.path;
Future<void> _writeLinuxPluginCmakefile(File destinationFile, Map<String, dynamic> templateContext) async {
_renderTemplateToFile(
_linuxPluginMakefileTemplate,
_linuxPluginCmakefileTemplate,
templateContext,
globals.fs.path.join(registryDirectory, 'generated_plugins.mk'),
destinationFile.path,
);
}
......
......@@ -1068,15 +1068,15 @@ class LinuxProject extends FlutterProjectPlatform {
@override
bool existsSync() => _editableDirectory.existsSync();
/// The Linux project makefile.
File get makeFile => _editableDirectory.childFile('Makefile');
/// The Linux project CMake specification.
File get cmakeFile => _editableDirectory.childFile('CMakeLists.txt');
/// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
/// the build.
File get generatedMakeConfigFile => ephemeralDirectory.childFile('generated_config.mk');
File get generatedCmakeConfigFile => ephemeralDirectory.childFile('generated_config.cmake');
/// Makefile with rules and variables for plugin builds.
File get generatedPluginMakeFile => managedDirectory.childFile('generated_plugins.mk');
/// Includable CMake with rules and variables for plugin builds.
File get generatedPluginCmakeFile => managedDirectory.childFile('generated_plugins.cmake');
/// The directory to write plugin symlinks.
Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks');
......
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
set(BINARY_NAME "{{projectName}}")
set(CMAKE_INSTALL_RPATH "\$ORIGIN")
# Configure build options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build
add_executable(${BINARY_NAME}
"main.cc"
"window_configuration.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
apply_standard_settings(${BINARY_NAME})
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
add_dependencies(${BINARY_NAME} flutter_assemble)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${PROJECT_BINARY_DIR}/bundle" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
file(GLOB FLUTTER_PLUGIN_DIRS "${PROJECT_BINARY_DIR}/plugins/*")
INSTALL(CODE "
include(BundleUtilities)
fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/${BINARY_NAME}\" \"\"
\"${FLUTTER_PLUGIN_DIRS};${FLUTTER_LIBRARY_DIR}\")
" COMPONENT Runtime)
INSTALL(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
INSTALL(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
INSTALL(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
include app_configuration.mk
# Default build type.
BUILD=debug
FLUTTER_MANAGED_DIR=flutter
FLUTTER_EPHEMERAL_DIR=$(FLUTTER_MANAGED_DIR)/ephemeral
# Configuration provided via flutter tool.
FLUTTER_CONFIG_FILE=$(FLUTTER_EPHEMERAL_DIR)/generated_config.mk
include $(FLUTTER_CONFIG_FILE)
# Dependency locations
FLUTTER_APP_DIR=$(CURDIR)/..
FLUTTER_APP_BUILD_DIR=$(FLUTTER_APP_DIR)/build
OUT_DIR=$(FLUTTER_APP_BUILD_DIR)/linux
OBJ_DIR=$(OUT_DIR)/obj/$(BUILD)
# Libraries
FLUTTER_LIB_NAME=flutter_linux_glfw
FLUTTER_LIB=$(FLUTTER_EPHEMERAL_DIR)/lib$(FLUTTER_LIB_NAME).so
# Tools
FLUTTER_BIN=$(FLUTTER_ROOT)/bin/flutter
LINUX_BUILD=$(FLUTTER_ROOT)/packages/flutter_tools/bin/tool_backend.sh
# Resources
ICU_DATA_NAME=icudtl.dat
ICU_DATA_SOURCE=$(FLUTTER_EPHEMERAL_DIR)/$(ICU_DATA_NAME)
FLUTTER_ASSETS_NAME=flutter_assets
FLUTTER_ASSETS_SOURCE=$(FLUTTER_APP_BUILD_DIR)/$(FLUTTER_ASSETS_NAME)
# Bundle structure
BUNDLE_OUT_DIR=$(OUT_DIR)/$(BUILD)
BUNDLE_DATA_DIR=$(BUNDLE_OUT_DIR)/data
BUNDLE_LIB_DIR=$(BUNDLE_OUT_DIR)/lib
BIN_OUT=$(BUNDLE_OUT_DIR)/$(BINARY_NAME)
ICU_DATA_OUT=$(BUNDLE_DATA_DIR)/$(ICU_DATA_NAME)
FLUTTER_LIB_OUT=$(BUNDLE_LIB_DIR)/$(notdir $(FLUTTER_LIB))
ALL_LIBS_OUT=$(FLUTTER_LIB_OUT) \
$(foreach lib,$(EXTRA_BUNDLED_LIBRARIES),$(BUNDLE_LIB_DIR)/$(notdir $(lib)))
# Add relevant code from the wrapper library, which is intended to be statically
# built into the client.
# Use abspath for the wrapper root, which can contain relative paths; the
# intermediate build files will be based on the source path, which will cause
# issues if they start with one or more '../'s.
WRAPPER_ROOT=$(abspath $(FLUTTER_EPHEMERAL_DIR)/cpp_client_wrapper_glfw)
WRAPPER_SOURCES= \
$(WRAPPER_ROOT)/flutter_engine.cc \
$(WRAPPER_ROOT)/flutter_window_controller.cc \
$(WRAPPER_ROOT)/plugin_registrar.cc \
$(WRAPPER_ROOT)/engine_method_result.cc
# Use abspath for extra sources, which may also contain relative paths (see
# note above about WRAPPER_ROOT).
SOURCES=main.cc window_configuration.cc \
flutter/generated_plugin_registrant.cc \
$(WRAPPER_SOURCES) $(abspath $(EXTRA_SOURCES))
# Headers
WRAPPER_INCLUDE_DIR=$(WRAPPER_ROOT)/include
INCLUDE_DIRS=$(FLUTTER_EPHEMERAL_DIR) $(WRAPPER_INCLUDE_DIR)
# Build settings
ifneq ($(strip $(SYSTEM_LIBRARIES)),)
EXTRA_CPPFLAGS+=$(patsubst -I%,-isystem%,$(shell pkg-config --cflags $(SYSTEM_LIBRARIES)))
EXTRA_LDFLAGS+=$(shell pkg-config --libs $(SYSTEM_LIBRARIES))
endif
CXX=clang++
CPPFLAGS.release=-DNDEBUG
CPPFLAGS.profile=$(CPPFLAGS.release)
CXXFLAGS.release=-O2
CXXFLAGS.profile=$(CXXFLAGS.release)
CXXFLAGS=-std=c++14 -Wall -Werror $(CXXFLAGS.$(BUILD)) $(EXTRA_CXXFLAGS)
CPPFLAGS=$(patsubst %,-I%,$(INCLUDE_DIRS)) \
$(CPPFLAGS.$(BUILD)) $(EXTRA_CPPFLAGS)
LDFLAGS=-L$(BUNDLE_LIB_DIR) \
-l$(FLUTTER_LIB_NAME) \
$(EXTRA_LDFLAGS) \
-Wl,-rpath=\$$ORIGIN/lib
# Intermediate files.
OBJ_FILES=$(SOURCES:%.cc=$(OBJ_DIR)/%.o)
DEPENDENCY_FILES=$(OBJ_FILES:%.o=%.d)
# Targets
.PHONY: all
all: $(BIN_OUT) bundle
# Add the plugin targets, and their associated settings.
include $(FLUTTER_MANAGED_DIR)/generated_plugins.mk
EXTRA_BUNDLED_LIBRARIES+=$(PLUGIN_LIBRARIES)
EXTRA_LDFLAGS+=$(PLUGIN_LDFLAGS)
EXTRA_CPPFLAGS+=$(PLUGIN_CPPFLAGS)
# This is a phony target because the flutter tool cannot describe
# its inputs and outputs yet.
.PHONY: sync
sync: $(FLUTTER_CONFIG_FILE)
$(LINUX_BUILD) linux-x64 $(BUILD)
.PHONY: bundle
bundle: $(ICU_DATA_OUT) $(ALL_LIBS_OUT) bundleflutterassets
$(BIN_OUT): $(OBJ_FILES) $(ALL_LIBS_OUT)
mkdir -p $(@D)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(OBJ_FILES) $(LDFLAGS) -o $@
$(WRAPPER_SOURCES) $(FLUTTER_LIB) $(ICU_DATA_SOURCE) $(FLUTTER_ASSETS_SOURCE) \
$(PLUGIN_TARGETS): | sync
# Plugin library bundling pattern.
$(BUNDLE_LIB_DIR)/%: $(OUT_DIR)/%
mkdir -p $(BUNDLE_LIB_DIR)
cp $< $@
$(FLUTTER_LIB_OUT): $(FLUTTER_LIB)
mkdir -p $(@D)
cp $< $@
$(ICU_DATA_OUT): $(ICU_DATA_SOURCE)
mkdir -p $(@D)
cp $< $@
-include $(DEPENDENCY_FILES)
$(OBJ_DIR)/%.o : %.cc | sync
mkdir -p $(@D)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -MMD -c $< -o $@
# Fully re-copy the assets directory on each build to avoid having to keep a
# comprehensive list of all asset files here, which would be fragile to changes
# in other files (e.g., adding a new font to pubspec.yaml).
.PHONY: bundleflutterassets
bundleflutterassets: $(FLUTTER_ASSETS_SOURCE)
mkdir -p $(BUNDLE_DATA_DIR)
rsync -rpu --delete $(FLUTTER_ASSETS_SOURCE) $(BUNDLE_DATA_DIR)
.PHONY: clean
clean:
rm -rf $(OUT_DIR); \
cd $(FLUTTER_APP_DIR); \
$(FLUTTER_BIN) clean
# This file contains variables that applications are likely to need to
# change, to isolate them from the main Makefile where the build rules are still
# in flux. This should simplify re-creating the runner while preserving local
# changes.
# Executable name.
BINARY_NAME={{projectName}}
# Any extra source files to build.
EXTRA_SOURCES=
# Paths of any additional libraries to be bundled in the output directory.
EXTRA_BUNDLED_LIBRARIES=
# Extra flags (e.g., for library dependencies).
SYSTEM_LIBRARIES=
EXTRA_CXXFLAGS=
EXTRA_CPPFLAGS=
EXTRA_LDFLAGS=
\ No newline at end of file
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper_glfw")
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_glfw.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY_DIR "${EPHEMERAL_DIR}" PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_glfw.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
)
list(TRANSFORM FLUTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"engine_method_result.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_window_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
linux-x64 ${CMAKE_BUILD_TYPE}
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)
cmake_minimum_required(VERSION 3.10)
set(PROJECT_NAME "{{projectName}}")
project(${PROJECT_NAME} LANGUAGES CXX)
set(PLUGIN_NAME "${PROJECT_NAME}_plugin")
add_library(${PLUGIN_NAME} SHARED
"${PLUGIN_NAME}.cc"
)
apply_standard_settings(${PLUGIN_NAME})
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
include plugin_configuration.mk
# Default build type.
BUILD=debug
# Plugins must be provided a populated flutter/ephemeral/ dir. Normally this
# would be the ephemeral directory of the application using the plugin.
ifeq ($(strip $(FLUTTER_EPHEMERAL_DIR)),)
$(error FLUTTER_EPHEMERAL_DIR must be provided)
endif
# Dependency locations
# Default to building in the plugin directory.
OUT_DIR=$(CURDIR)/../build/linux
# Sharing an OUT_DIR will be common, so use a subdirectory for intermediates.
PLUGIN_OUT_DIR=$(OUT_DIR)/$(PLUGIN_NAME)
OBJ_DIR=$(PLUGIN_OUT_DIR)/obj/$(BUILD)
# Flutter library
FLUTTER_LIB_NAME=flutter_linux_glfw
FLUTTER_LIB=$(FLUTTER_EPHEMERAL_DIR)/lib$(FLUTTER_LIB_NAME).so
# Add relevant code from the wrapper library, which is intended to be statically
# built into the plugin.
# Use abspath for the wrapper root, which can contain relative paths; the
# intermediate build files will be based on the source path, which will cause
# issues if they start with one or more '../'s.
WRAPPER_ROOT=$(abspath $(FLUTTER_EPHEMERAL_DIR)/cpp_client_wrapper_glfw)
# TODO: Once JSON codec files are merged, make a PLUGIN_CODEC variable in the
# top section. For now, using JSON codec would require changes here.
WRAPPER_SOURCES= \
$(WRAPPER_ROOT)/engine_method_result.cc \
$(WRAPPER_ROOT)/plugin_registrar.cc \
$(WRAPPER_ROOT)/standard_codec.cc
# Use abspath for extra sources, which may also contain relative paths (see
# note above about WRAPPER_ROOT).
SOURCES=$(PLUGIN_NAME)_plugin.cc $(WRAPPER_SOURCES) $(abspath $(EXTRA_SOURCES))
WRAPPER_INCLUDE_DIR=$(WRAPPER_ROOT)/include
INCLUDE_DIRS=$(FLUTTER_EPHEMERAL_DIR) $(WRAPPER_INCLUDE_DIR)
# Build settings
CXX=clang++
CPPFLAGS.release=-DNDEBUG
CPPFLAGS.profile=$(CPPFLAGS.release)
CXXFLAGS.release=-O2
CXXFLAGS.profile=$(CXXFLAGS.release)
CXXFLAGS=-std=c++14 -Wall -Werror -fPIC -fvisibility=hidden \
$(CXXFLAGS.$(BUILD)) $(EXTRA_CXXFLAGS)
CPPFLAGS=-DFLUTTER_PLUGIN_IMPL $(patsubst %,-I%,$(INCLUDE_DIRS)) \
$(CPPFLAGS.$(BUILD)) $(EXTRA_CPPFLAGS)
LDFLAGS=-shared -L$(FLUTTER_EPHEMERAL_DIR) -l$(FLUTTER_LIB_NAME) $(EXTRA_LDFLAGS)
# Final output files that will be used by applications.
LIBRARY_OUT=$(OUT_DIR)/lib$(PLUGIN_NAME)_plugin.so
# Intermediate files.
OBJ_FILES=$(SOURCES:%.cc=$(OBJ_DIR)/%.o)
DEPENDENCY_FILES=$(OBJ_FILES:%.o=%.d)
# Targets
.PHONY: all
all: $(PLUGIN_NAME)
.PHONY: $(PLUGIN_NAME)
$(PLUGIN_NAME) : $(LIBRARY_OUT)
$(LIBRARY_OUT): $(OBJ_FILES)
mkdir -p $(@D)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $^ $(LDFLAGS) -o $@
-include $(DEPENDENCY_FILES)
$(OBJ_DIR)/%.o : %.cc
mkdir -p $(@D)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -MMD -c $< -o $@
.PHONY: clean
clean:
rm -f $(LIBRARY_OUT)
rm -rf $(PLUGINOUT_DIR)
# The name of the plugin.
# The primary source file is assumed to be $(PLUGIN_NAME)_plugin.cc, with
# $(PLUGIN_NAME)_plugin.h as the public header meant for inclusion by the
# application.
PLUGIN_NAME={{projectName}}
# Any files other than the plugin class files that need to be compiled.
EXTRA_SOURCES=
# Extra flags (e.g., for library dependencies).
EXTRA_CXXFLAGS=
EXTRA_CPPFLAGS=
EXTRA_LDFLAGS=
\ No newline at end of file
#include "{{projectName}}_plugin.h"
#include "include/{{projectName}}_plugin.h"
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_glfw.h>
......
......@@ -634,7 +634,7 @@ void main() {
await runner.run(<String>['create', '--no-pub', projectDir.path]);
expect(projectDir.childDirectory('linux').childFile('Makefile').existsSync(), true);
expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt').existsSync(), true);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
});
......@@ -649,7 +649,7 @@ void main() {
await runner.run(<String>['create', '--no-pub', projectDir.path]);
expect(projectDir.childDirectory('linux').childFile('Makefile').existsSync(), false);
expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt').existsSync(), false);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: false),
});
......@@ -664,7 +664,7 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
expect(projectDir.childDirectory('linux').childFile('Makefile').existsSync(), true);
expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt').existsSync(), true);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
});
......@@ -679,7 +679,7 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
expect(projectDir.childDirectory('linux').childFile('Makefile').existsSync(), false);
expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt').existsSync(), false);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: false),
});
......
......@@ -91,8 +91,8 @@ void main() {
when(linuxProject.managedDirectory).thenReturn(linuxManagedDirectory);
when(linuxProject.ephemeralDirectory).thenReturn(linuxEphemeralDirectory);
when(linuxProject.pluginSymlinkDirectory).thenReturn(linuxEphemeralDirectory.childDirectory('.plugin_symlinks'));
when(linuxProject.makeFile).thenReturn(linuxManagedDirectory.parent.childFile('Makefile'));
when(linuxProject.generatedPluginMakeFile).thenReturn(linuxManagedDirectory.childFile('generated_plugins.mk'));
when(linuxProject.cmakeFile).thenReturn(linuxManagedDirectory.parent.childFile('CMakeLists.txt'));
when(linuxProject.generatedPluginCmakeFile).thenReturn(linuxManagedDirectory.childFile('generated_plugins.mk'));
when(linuxProject.existsSync()).thenReturn(false);
when(mockClock.now()).thenAnswer(
......@@ -961,7 +961,7 @@ flutter:
FeatureFlags: () => featureFlags,
});
testUsingContext('Injecting creates generated Linux plugin makefile', () async {
testUsingContext('Injecting creates generated Linux plugin Cmake file', () async {
when(linuxProject.existsSync()).thenReturn(true);
when(featureFlags.isLinuxEnabled).thenReturn(true);
when(flutterProject.isModule).thenReturn(false);
......@@ -969,16 +969,12 @@ flutter:
await injectPlugins(flutterProject, checkProjects: true);
final File pluginMakefile = linuxProject.generatedPluginMakeFile;
final File pluginMakefile = linuxProject.generatedPluginCmakeFile;
expect(pluginMakefile.existsSync(), isTrue);
final String contents = pluginMakefile.readAsStringSync();
expect(contents, contains('libapackage_plugin.so'));
// Verify all the variables the app-level Makefile rely on.
expect(contents, contains('PLUGIN_TARGETS='));
expect(contents, contains('PLUGIN_LIBRARIES='));
expect(contents, contains('PLUGIN_LDFLAGS='));
expect(contents, contains('PLUGIN_CPPFLAGS='));
expect(contents, contains('apackage'));
expect(contents, contains('target_link_libraries(\${BINARY_NAME} PRIVATE \${plugin}_plugin)'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment