diff --git a/pkg/dartdev/lib/src/commands/compile.dart b/pkg/dartdev/lib/src/commands/compile.dart index cecd0cc4ee0..a3b4341bcd8 100644 --- a/pkg/dartdev/lib/src/commands/compile.dart +++ b/pkg/dartdev/lib/src/commands/compile.dart @@ -26,6 +26,7 @@ import '../vm_interop_handler.dart'; const int genericErrorExitCode = 255; const int compileErrorExitCode = 254; +const int crossCompileErrorExitCode = 128; class Option { final String flag; @@ -597,7 +598,7 @@ Remove debugging information from the output and save it separately to the speci stderr.writeln('Unsupported target platform $target.'); stderr.writeln('Supported target platforms: ' '${supportedTargetPlatforms.join(', ')}'); - return 128; + return crossCompileErrorExitCode; } var cacheDir = getDartStorageDirectory(); diff --git a/pkg/dartdev/test/commands/compile_test.dart b/pkg/dartdev/test/commands/compile_test.dart index 975beaac18c..b5e237c56d3 100644 --- a/pkg/dartdev/test/commands/compile_test.dart +++ b/pkg/dartdev/test/commands/compile_test.dart @@ -8,14 +8,14 @@ import 'dart:math'; import 'package:code_assets/code_assets.dart'; import 'package:dart2native/dart2native_macho.dart' show pipeStream; import 'package:dart2native/macho.dart'; +import 'package:dartdev/src/commands/compile.dart' + show compileErrorExitCode, genericErrorExitCode; import 'package:hooks_runner/hooks_runner.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; import '../utils.dart'; -const int compileErrorExitCode = 254; - void main() { ensureRunFromSdkBinDart(); @@ -27,21 +27,13 @@ const String soundNullSafetyWarning = "Warning: Option '--sound-null-safety' is deprecated."; const String failedAssertionError = 'Failed assertion: line'; -String usingTargetOSMessageForPlatform(String targetOS) => +String usingTargetOSMessage(OS targetOS) => 'Specializing Platform getters for target OS $targetOS.'; -final String usingTargetOSMessage = - usingTargetOSMessageForPlatform(Platform.operatingSystem); -String unsupportedTargetError(Target target) => - 'Unsupported target platform $target'; +final String targetingHostOSMessage = usingTargetOSMessage(Target.current.os); void defineCompileTests() { - final host = Target.current; // AOT compilation is not available on IA32. - final bool isRunningOnIA32 = host.architecture == Architecture.ia32; - // Cross compilation is not available on 32-bit architectures. - final bool isRunningOn32Bit = isRunningOnIA32 || - host.architecture == Architecture.arm || - host.architecture == Architecture.riscv32; + final bool isRunningOnIA32 = Target.current.architecture == Architecture.ia32; if (Platform.isMacOS) { test('Compile exe for MacOS signing', () async { @@ -266,7 +258,7 @@ void defineCompileTests() { '-v', inFile, ]); - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stderr, isNot(contains(soundNullSafetyMessage))); expect(result.exitCode, 0); final file = File(outFile); @@ -294,7 +286,7 @@ void defineCompileTests() { ); // Executables should be (host) OS-specific by default. - expect(result.stdout, contains(usingTargetOSMessage)); + expect(result.stdout, contains(targetingHostOSMessage)); expect(result.stderr, isEmpty); expect(result.exitCode, 0); expect(File(outFile).existsSync(), true, @@ -365,7 +357,7 @@ void defineCompileTests() { ], ); - expect(result.stdout, contains(usingTargetOSMessage)); + expect(result.stdout, contains(targetingHostOSMessage)); expect(result.stderr, isEmpty); expect(result.exitCode, 0); expect(File(outFile).existsSync(), true, @@ -418,95 +410,6 @@ void defineCompileTests() { expect(result.exitCode, 0); }, skip: isRunningOnIA32); - Future basicCrossCompileTest(Target target) async { - final p = project( - mainSrc: 'void main() {print(const String.fromEnvironment("cross"));}'); - final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); - final outFile = path.canonicalize(path.join(p.dirPath, 'myexe')); - final result = await p.run( - [ - 'compile', - 'exe', - '-v', - '--target-os', - target.os.name, - '--target-arch', - target.architecture.name, - '-o', - outFile, - inFile, - ], - ); - - if (result.exitCode != 0) { - print('Subcommand terminated with exit code ${result.exitCode}.'); - if (result.stdout.isNotEmpty) { - print('Subcommand stdout:'); - print(result.stdout); - } - if (result.stderr.isNotEmpty) { - print('Subcommand stderr:'); - print(result.stderr); - } - } - - expect(result.stdout, - contains(usingTargetOSMessageForPlatform(target.os.name))); - expect(result.stderr, isNot(contains(unsupportedTargetError(target)))); - expect(result.exitCode, 0); - } - - final crossCompileOSes = [ - OS.linux, - ]; - - final crossCompileArchitectures = [ - Architecture.arm, - Architecture.arm64, - Architecture.riscv64, - Architecture.x64, - ]; - - for (final os in crossCompileOSes) { - for (final arch in crossCompileArchitectures) { - if (os == host.os && arch == host.architecture) continue; - final target = Target.fromArchitectureAndOS(arch, os); - test('Compile executable can cross compile to $os $arch', - () async => basicCrossCompileTest(target), - skip: isRunningOn32Bit); - } - } - - test('Compile executable cannot compile cross-OS', () async { - final p = project( - mainSrc: 'void main() {print(const String.fromEnvironment("cross"));}'); - final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); - final outFile = path.canonicalize(path.join(p.dirPath, 'myexe')); - // Make sure targetOS is always unsupported (not Linux and not matches host - // OS) to trigger an error. - final targetOS = Platform.isWindows ? OS.macOS : OS.windows; - final targetArch = Architecture.arm64; - final target = Target.fromArchitectureAndOS(targetArch, targetOS); - final result = await p.run( - [ - 'compile', - 'exe', - '-v', - '--target-os', - targetOS.name, - '--target-arch', - targetArch.name, - '-o', - outFile, - inFile, - ], - ); - - expect(result.stdout, isNot(contains(usingTargetOSMessage))); - expect(result.stderr, contains(unsupportedTargetError(target))); - expect(result.exitCode, 128); - }, skip: isRunningOnIA32); - test('Compile and run aot snapshot', () async { final p = project(mainSrc: 'void main() { print("I love AOT"); }'); final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); @@ -524,7 +427,7 @@ void defineCompileTests() { ); // AOT snapshots should be OS-specific by default. - expect(result.stdout, contains(usingTargetOSMessage)); + expect(result.stdout, contains(targetingHostOSMessage)); expect(result.stderr, isEmpty); expect(result.exitCode, 0); expect(File(outFile).existsSync(), true, @@ -541,73 +444,6 @@ void defineCompileTests() { expect(result.exitCode, 0); }, skip: isRunningOnIA32); - test('Compile aot snapshot can compile to host platform', () async { - final targetOS = Platform.operatingSystem; - final p = project(mainSrc: 'void main() { print("I love $targetOS"); }'); - final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); - final outFile = path.canonicalize(path.join(p.dirPath, 'main.aot')); - - var result = await p.run( - [ - 'compile', - 'aot-snapshot', - '-v', - '--target-os', - targetOS, - '-o', - 'main.aot', - inFile, - ], - ); - - expect(result.stdout, contains(usingTargetOSMessageForPlatform(targetOS))); - expect(result.stderr, isEmpty); - expect(result.exitCode, 0); - expect(File(outFile).existsSync(), true, - reason: 'File not found: $outFile'); - - final Directory binDir = File(Platform.resolvedExecutable).parent; - result = Process.runSync( - path.join(binDir.path, 'dartaotruntime'), - [outFile], - ); - - expect(result.stdout, contains('I love $targetOS')); - expect(result.stderr, isEmpty); - expect(result.exitCode, 0); - }, skip: isRunningOnIA32); - - test('Compile aot snapshot cannot compile cross platform', () async { - // Make sure targetOS is always unsupported (not Linux and not matches host - // OS) to trigger an error. - final targetOS = Platform.isWindows ? OS.macOS : OS.windows; - final targetArch = Architecture.arm64; - final target = Target.fromArchitectureAndOS(targetArch, targetOS); - final p = project(mainSrc: 'void main() { print("I love $targetOS"); }'); - final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); - - var result = await p.run( - [ - 'compile', - 'aot-snapshot', - '-v', - '--target-os', - targetOS.name, - '--target-arch', - targetArch.name, - '-o', - 'main.aot', - inFile, - ], - ); - - expect(result.stdout, - isNot(contains(usingTargetOSMessageForPlatform(targetOS.name)))); - expect(result.stderr, contains(unsupportedTargetError(target))); - - expect(result.exitCode, isNot(0)); - }, skip: isRunningOnIA32); - test('Compile and run kernel snapshot', () async { final p = project(mainSrc: 'void main() { print("I love kernel"); }'); final outFile = path.join(p.dirPath, 'main.dill'); @@ -623,7 +459,7 @@ void defineCompileTests() { ); expect(File(outFile).existsSync(), true, reason: 'File not found: $outFile'); - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stderr, isNot(contains(soundNullSafetyMessage))); expect(result.exitCode, 0); @@ -653,7 +489,7 @@ void defineCompileTests() { '-v', inFile, ]); - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); final file = File(outFile); @@ -683,7 +519,7 @@ void defineCompileTests() { outFile, inFile, ]); - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); final file = File(outFile); @@ -853,7 +689,7 @@ void main() { ); // Only printed when -v/--verbose is used, not --verbosity. - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stdout, isNot(contains(soundNullSafetyMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); @@ -883,7 +719,7 @@ void main() { ); // Only printed when -v/--verbose is used, not --verbosity. - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stdout, isNot(contains(soundNullSafetyMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); @@ -910,7 +746,7 @@ void main() { ); // Only printed when -v/--verbose is used, not --verbosity. - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stdout, isNot(contains(soundNullSafetyMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); @@ -977,7 +813,7 @@ void main() {} expect(result.stderr, contains('Error: The output file "foo" does not end with ".wasm"')); - expect(result.exitCode, 255); + expect(result.exitCode, genericErrorExitCode); }, skip: isRunningOnIA32); test('Compile wasm with error', () async { @@ -1166,7 +1002,7 @@ void main() { ); // Only printed when -v/--verbose is used, not --verbosity. - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stdout, isNot(contains(soundNullSafetyMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); @@ -1196,7 +1032,7 @@ void main() { ); // Only printed when -v/--verbose is used, not --verbosity. - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stdout, isNot(contains(soundNullSafetyMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); @@ -1223,7 +1059,7 @@ void main() { ); // Only printed when -v/--verbose is used, not --verbosity. - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stdout, isNot(contains(soundNullSafetyMessage))); expect(result.stderr, isEmpty); expect(result.exitCode, 0); @@ -1299,7 +1135,7 @@ void main() {} (dynamic o) => '$o'.contains('Unable to open file'), ), ); - expect(result.exitCode, 255); + expect(result.exitCode, genericErrorExitCode); }); test('Compile kernel with invalid trailing argument', () async { @@ -1347,7 +1183,7 @@ void main() {} ], ); - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stderr, isNot(contains(soundNullSafetyMessage))); expect(result.exitCode, 0); expect(File(outFile).existsSync(), true, @@ -1567,10 +1403,10 @@ void main() { ); // Only printed when -v/--verbose is used, not --verbosity. - expect(result.stdout, isNot(contains(usingTargetOSMessage))); + expect(result.stdout, isNot(contains(targetingHostOSMessage))); expect(result.stdout, isNot(contains(soundNullSafetyMessage))); expect(result.stderr, contains(failedAssertionError)); - expect(result.exitCode, 255); + expect(result.exitCode, genericErrorExitCode); result = await p.run( ['--enable-asserts', outFile], diff --git a/pkg/dartdev/test/commands/cross_compile_test.dart b/pkg/dartdev/test/commands/cross_compile_test.dart new file mode 100644 index 00000000000..d1570e71097 --- /dev/null +++ b/pkg/dartdev/test/commands/cross_compile_test.dart @@ -0,0 +1,144 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:dartdev/src/commands/compile.dart' + show CompileNativeCommand, crossCompileErrorExitCode; +import 'package:hooks_runner/hooks_runner.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import '../utils.dart'; +import 'compile_test.dart' show targetingHostOSMessage, usingTargetOSMessage; + +void main() { + ensureRunFromSdkBinDart(); + + // Cross compilation is not available on 32-bit architectures. + final hostArch = Target.current.architecture; + final bool isRunningOn32Bit = hostArch == Architecture.ia32 || + hostArch == Architecture.arm || + hostArch == Architecture.riscv32; + + group('cross compile -', defineCrossCompileTests, + timeout: longTimeout, skip: isRunningOn32Bit); +} + +String unsupportedTargetMessage(Target target) => + 'Unsupported target platform $target'; +typedef TestFunction = Future Function(); + +void defineCrossCompileTests() { + final subcommands = [ + CompileNativeCommand.aotSnapshotCmdName, + CompileNativeCommand.exeCmdName, + ]; + final crossCompileTargets = CompileNativeCommand.supportedTargetPlatforms; + String mainMessage(Target target) => 'I love ${target.os}'; + + Future<(ProcessResult, String)> crossCompile( + String subcommand, Target target) async { + final p = + project(mainSrc: 'void main() {print("${mainMessage(target)}");}'); + final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); + final filename = subcommand == CompileNativeCommand.exeCmdName + ? 'myexe' + : subcommand == CompileNativeCommand.aotSnapshotCmdName + ? 'out.so' + : throw ArgumentError( + 'Unexpected subcommand $subcommand', 'subcommand'); + final outFile = path.canonicalize(path.join(p.dirPath, filename)); + final result = await p.run( + [ + 'compile', + subcommand, + '-v', + '--target-os', + target.os.name, + '--target-arch', + target.architecture.name, + '-o', + outFile, + inFile, + ], + ); + + print('Subcommand terminated with exit code ${result.exitCode}.'); + if (result.stdout.isNotEmpty) { + print('Subcommand stdout:'); + print(result.stdout); + } + if (result.stderr.isNotEmpty) { + print('Subcommand stderr:'); + print(result.stderr); + } + + return (result, outFile); + } + + TestFunction crossCompileTest(String subcommand, Target target) => () async { + expect(subcommand, isIn(subcommands)); + expect(target, isIn(crossCompileTargets)); + var (result, outFile) = await crossCompile(subcommand, target); + + expect(result.stdout, contains(usingTargetOSMessage(target.os))); + expect( + result.stderr, isNot(contains(unsupportedTargetMessage(target)))); + expect(result.exitCode, 0); + expect(File(outFile).existsSync(), true, + reason: 'File not found: $outFile'); + + if (target != Target.current) return; + + if (subcommand == CompileNativeCommand.exeCmdName) { + result = Process.runSync(outFile, const []); + } else { + expect(subcommand, CompileNativeCommand.aotSnapshotCmdName); + final Directory binDir = File(Platform.resolvedExecutable).parent; + result = Process.runSync( + path.join(binDir.path, 'dartaotruntime'), + [outFile], + ); + } + + expect(result.stdout, contains(mainMessage(target))); + expect(result.stderr, isEmpty); + expect(result.exitCode, 0); + }; + + TestFunction crossCompileFailureTest(String subcommand, Target target) => + () async { + expect(subcommand, isIn(subcommands)); + expect(target, isNot(Target.current)); + expect(target, isNot(isIn(crossCompileTargets))); + final (result, outFile) = await crossCompile(subcommand, target); + + expect(result.stdout, isNot(contains(targetingHostOSMessage))); + expect(result.stdout, isNot(contains(usingTargetOSMessage(target.os)))); + expect(result.stderr, contains(unsupportedTargetMessage(target))); + expect(result.exitCode, crossCompileErrorExitCode); + expect(File(outFile).existsSync(), false, + reason: 'File created despite failure: $outFile'); + }; + + for (final subcommand in subcommands) { + for (final target in crossCompileTargets) { + test('Compile $subcommand can cross compile to $target', + crossCompileTest(subcommand, target)); + } + var targetOS = Platform.isWindows ? OS.macOS : OS.windows; + var targetArch = Architecture.arm64; + var target = Target.fromArchitectureAndOS(targetArch, targetOS); + test('Compile $subcommand fails on invalid target OS', + crossCompileFailureTest(subcommand, target)); + + targetOS = OS.linux; + targetArch = Architecture.riscv32; + target = Target.fromArchitectureAndOS(targetArch, targetOS); + test('Compile $subcommand fails on invalid target architecture', + crossCompileFailureTest(subcommand, target)); + } +}