@@ -7,111 +7,141 @@ import 'dart:io';
7
7
import 'package:args/args.dart' ;
8
8
import 'package:package_config/package_config.dart' ;
9
9
10
- const _buildDir = "build-dir" ;
11
- const _srcDir = "source-dir" ;
10
+ const ansiRed = '\x 1b[31m' ;
11
+ const ansiDefault = '\x 1b[39;49m' ;
12
+
13
+ const _defaultRelativeBuildPath = "build/jni_libs" ;
14
+
15
+ const _buildPath = "build-path" ;
16
+ const _srcPath = "source-path" ;
12
17
const _packageName = 'package-name' ;
13
18
const _verbose = "verbose" ;
14
19
const _cmakeArgs = "cmake-args" ;
15
- const _clean = "clean" ;
16
-
17
- const _cmakeTemporaryFiles = [
18
- 'CMakeCache.txt' ,
19
- 'CMakeFiles/' ,
20
- 'cmake_install.cmake' ,
21
- 'Makefile'
22
- ];
23
-
24
- void deleteCMakeTemps (Uri buildDir) async {
25
- for (var filename in _cmakeTemporaryFiles) {
26
- if (options.verbose) {
27
- stderr.writeln ('remove $filename ' );
28
- }
29
- await File (buildDir.resolve (filename).toFilePath ()).delete (recursive: true );
20
+
21
+ Future <void > runCommand (
22
+ String exec, List <String > args, String workingDir) async {
23
+ // For printing relative path always.
24
+ var current = Directory .current.path;
25
+ if (! current.endsWith (Platform .pathSeparator)) {
26
+ current += Platform .pathSeparator;
27
+ }
28
+ if (workingDir.startsWith (current)) {
29
+ workingDir.replaceFirst (current, "" );
30
30
}
31
- }
32
31
33
- // Sets up input output channels and maintains state.
34
- class CommandRunner {
35
- CommandRunner ({this .printCmds = false });
36
- bool printCmds = false ;
37
- int ? time;
38
- Future <CommandRunner > run (
39
- String exec, List <String > args, String workingDir) async {
40
- if (printCmds) {
41
- final cmd = "$exec ${args .join (" " )}" ;
42
- stderr.writeln ("\n + [$workingDir ] $cmd " );
43
- }
44
- final process = await Process .start (exec, args,
45
- workingDirectory: workingDir,
46
- runInShell: true ,
47
- mode: ProcessStartMode .inheritStdio);
48
- final exitCode = await process.exitCode;
32
+ final cmd = "$exec ${args .join (" " )}" ;
33
+ stderr.writeln ("+ [$workingDir ] $cmd " );
34
+ int exitCode;
35
+ if (options.verbose) {
36
+ final process = await Process .start (
37
+ exec, args,
38
+ workingDirectory: workingDir,
39
+ mode: ProcessStartMode .inheritStdio,
40
+ // without `runInShell`, sometimes cmake doesn't run on windows.
41
+ runInShell: true ,
42
+ );
43
+ exitCode = await process.exitCode;
44
+ } else {
45
+ // ProcessStartMode.normal sometimes hangs on windows. No idea why.
46
+ final process = await Process .run (exec, args,
47
+ runInShell: true , workingDirectory: workingDir);
48
+ exitCode = process.exitCode;
49
49
if (exitCode != 0 ) {
50
- stderr.writeln ("command exited with $exitCode " );
50
+ var out = process.stdout;
51
+ var err = process.stderr;
52
+ if (stdout.supportsAnsiEscapes) {
53
+ out = "$ansiRed $out $ansiDefault " ;
54
+ err = "$ansiRed $err $ansiDefault " ;
55
+ }
56
+ stdout.writeln (out);
57
+ stderr.writeln (err);
51
58
}
52
- return this ;
59
+ }
60
+ if (exitCode != 0 ) {
61
+ stderr.writeln ("Command exited with $exitCode ." );
53
62
}
54
63
}
55
64
56
65
class Options {
57
66
Options (ArgResults arg)
58
- : buildDir = arg[_buildDir ],
59
- srcDir = arg[_srcDir ],
67
+ : buildPath = arg[_buildPath ],
68
+ srcPath = arg[_srcPath ],
60
69
packageName = arg[_packageName] ?? 'jni' ,
61
70
cmakeArgs = arg[_cmakeArgs],
62
- verbose = arg[_verbose] ?? false ,
63
- clean = arg[_clean] ?? false ;
71
+ verbose = arg[_verbose] ?? false ;
64
72
65
- String ? buildDir, srcDir ;
73
+ String ? buildPath, srcPath ;
66
74
String packageName;
67
75
List <String > cmakeArgs;
68
- bool verbose, clean ;
76
+ bool verbose;
69
77
}
70
78
71
79
late Options options;
72
- void log (String msg) {
80
+
81
+ void verboseLog (String msg) {
73
82
if (options.verbose) {
74
83
stderr.writeln (msg);
75
84
}
76
85
}
77
86
78
- /// tries to find package:jni's source folder in pub cache
79
- /// if not possible, returns null.
80
- Future <String ?> findSources () async {
87
+ /// Find path to C sources in pub cache for package specified by [packageName] .
88
+ ///
89
+ /// It's assumed C FFI sources are in "src/" relative to package root.
90
+ /// If package cannot be found, null is returned.
91
+ Future <String ?> findSources (String packageName) async {
81
92
final packageConfig = await findPackageConfig (Directory .current);
82
93
if (packageConfig == null ) {
83
94
return null ;
84
95
}
85
- final packages = packageConfig.packages;
86
- for (var package in packages) {
87
- if (package.name == options.packageName) {
88
- return package.root.resolve ("src/" ).toFilePath ();
96
+ final package = packageConfig[options.packageName];
97
+ if (package == null ) {
98
+ return null ;
99
+ }
100
+ return package.root.resolve ("src" ).toFilePath ();
101
+ }
102
+
103
+ /// Returns true if [artifact] does not exist, or any file in [sourceDir] is
104
+ /// newer than [artifact] .
105
+ bool needsBuild (File artifact, Directory sourceDir) {
106
+ if (! artifact.existsSync ()) return true ;
107
+ final fileLastModified = artifact.lastModifiedSync ();
108
+ for (final entry in sourceDir.listSync (recursive: true )) {
109
+ if (entry.statSync ().modified.isAfter (fileLastModified)) {
110
+ return true ;
89
111
}
90
112
}
91
- return null ;
113
+ return false ;
114
+ }
115
+
116
+ /// Returns the name of file built using sources in [cDir]
117
+ String getTargetName (Directory cDir) {
118
+ for (final file in cDir.listSync (recursive: true )) {
119
+ if (file.path.endsWith (".c" )) {
120
+ final cFileName = file.uri.pathSegments.last;
121
+ final librarySuffix = Platform .isWindows ? "dll" : "so" ;
122
+ return cFileName.substring (0 , cFileName.length - 1 ) + librarySuffix;
123
+ }
124
+ }
125
+ throw Exception ("Could not find a C file in ${cDir .path }" );
92
126
}
93
127
94
128
void main (List <String > arguments) async {
95
129
final parser = ArgParser ()
96
- ..addOption (_buildDir ,
97
- abbr: 'B ' , help: 'Directory to place built artifacts' )
98
- ..addOption (_srcDir ,
99
- abbr: 'S ' , help: 'alternative path to package:jni sources' )
130
+ ..addOption (_buildPath ,
131
+ abbr: 'b ' , help: 'Directory to place built artifacts' )
132
+ ..addOption (_srcPath ,
133
+ abbr: 's ' , help: 'alternative path to package:jni sources' )
100
134
..addOption (_packageName,
101
135
abbr: 'p' ,
102
136
help: 'package for which native'
103
137
'library should be built' ,
104
138
defaultsTo: 'jni' )
105
139
..addFlag (_verbose, abbr: 'v' , help: 'Enable verbose output' )
106
- ..addFlag (_clean,
107
- negatable: false ,
108
- abbr: 'C' ,
109
- help: 'Clear built artifacts instead of running a build' )
110
140
..addMultiOption (_cmakeArgs,
111
- abbr: 'm' , help: 'additional argument to pass to CMake' );
112
- final cli = parser.parse (arguments);
113
- options = Options (cli );
114
- final rest = cli .rest;
141
+ abbr: 'm' , help: 'Pass additional argument to CMake' );
142
+ final argResults = parser.parse (arguments);
143
+ options = Options (argResults );
144
+ final rest = argResults .rest;
115
145
116
146
if (rest.isNotEmpty) {
117
147
stderr.writeln ("one or more unrecognized arguments: $rest " );
@@ -121,79 +151,69 @@ void main(List<String> arguments) async {
121
151
return ;
122
152
}
123
153
124
- final srcPath = options.srcDir ?? await findSources ();
154
+ final srcPath = options.srcPath ?? await findSources (options.packageName );
125
155
126
156
if (srcPath == null ) {
127
- stderr.writeln ("No sources specified and current directory is not a "
128
- "package root ." );
157
+ stderr.writeln ("Cannot find sources for package ${ options . packageName } "
158
+ "and no sources were manually specified ." );
129
159
exitCode = 1 ;
130
160
return ;
131
161
}
132
162
133
163
final srcDir = Directory (srcPath);
134
- if (! await srcDir.exists () && ! options.clean) {
135
- throw 'Directory $srcPath does not exist' ;
164
+ if (! srcDir.existsSync ()) {
165
+ stderr.writeln ('Directory $srcPath does not exist' );
166
+ exitCode = 1 ;
167
+ return ;
136
168
}
137
169
138
- log ("srcPath: $srcPath " );
170
+ verboseLog ("srcPath: $srcPath " );
139
171
140
- final currentDirUri = Uri .file ("." );
141
- final buildPath =
142
- options.buildDir ?? currentDirUri.resolve ("build/jni_libs" ).toFilePath ();
172
+ final currentDirUri = Uri .directory ("." );
173
+ final buildPath = options.buildPath ??
174
+ currentDirUri.resolve (_defaultRelativeBuildPath ).toFilePath ();
143
175
final buildDir = Directory (buildPath);
144
176
await buildDir.create (recursive: true );
145
- log ("buildPath: $buildPath " );
177
+ verboseLog ("buildPath: $buildPath " );
146
178
147
179
if (buildDir.absolute.uri == srcDir.absolute.uri) {
148
180
stderr.writeln ("Please build in a directory different than source." );
149
- exit (2 );
181
+ exitCode = 1 ;
182
+ return ;
150
183
}
151
184
152
- if (options.clean) {
153
- await cleanup (options, srcDir.absolute.path, buildDir.absolute.path);
154
- } else {
155
- // pass srcDir absolute path because it will be passed to CMake as arg
156
- // which will be running in different directory
157
- final jniDirUri = Uri .directory (".dart_tool" ).resolve ("jni" );
158
- final jniDir = Directory .fromUri (jniDirUri);
159
- await jniDir.create (recursive: true );
160
- final tempDir = await jniDir.createTemp ("jni_native_build_" );
161
- await build (options, srcDir.absolute.path, tempDir.path);
162
- final dllDirUri =
163
- Platform .isWindows ? tempDir.uri.resolve ("Debug" ) : tempDir.uri;
164
- final dllDir = Directory .fromUri (dllDirUri);
165
- for (var entry in dllDir.listSync ()) {
166
- final dllSuffix = Platform .isWindows ? "dll" : "so" ;
167
- if (entry.path.endsWith (dllSuffix)) {
168
- final dllName = entry.uri.pathSegments.last;
169
- final target = buildDir.uri.resolve (dllName);
170
- entry.renameSync (target.toFilePath ());
171
- }
172
- }
173
- await tempDir.delete (recursive: true );
185
+ final targetFileUri = buildDir.uri.resolve (getTargetName (srcDir));
186
+ final targetFile = File .fromUri (targetFileUri);
187
+ if (! needsBuild (targetFile, srcDir)) {
188
+ verboseLog ("last modified of ${targetFile .path }: "
189
+ "${targetFile .lastModifiedSync ()}" );
190
+ stderr.writeln ("target newer than source, skipping build" );
191
+ return ;
174
192
}
175
- }
176
193
177
- Future <void > build (Options options, String srcPath, String buildPath) async {
178
- final runner = CommandRunner (printCmds: true );
194
+ // Note: creating temp dir in .dart_tool/jni instead of SystemTemp
195
+ // because latter can fail tests on Windows CI, when system temp is on
196
+ // separate drive or something.
197
+ final jniDirUri = Uri .directory (".dart_tool" ).resolve ("jni" );
198
+ final jniDir = Directory .fromUri (jniDirUri);
199
+ await jniDir.create (recursive: true );
200
+ final tempDir = await jniDir.createTemp ("jni_native_build_" );
179
201
final cmakeArgs = < String > [];
180
202
cmakeArgs.addAll (options.cmakeArgs);
181
- cmakeArgs.add (srcPath);
182
- await runner.run ("cmake" , cmakeArgs, buildPath);
183
- await runner.run ("cmake" , ["--build" , "." ], buildPath);
184
- }
185
-
186
- Future <void > cleanup (Options options, String srcPath, String buildPath) async {
187
- if (srcPath == buildPath) {
188
- stderr.writeln ('Error: build path is same as source path.' );
189
- }
190
-
191
- stderr.writeln ("deleting $buildPath " );
192
-
193
- try {
194
- await Directory (buildPath).delete (recursive: true );
195
- } catch (e) {
196
- stderr.writeln ("Error: cannot be deleted" );
197
- stderr.writeln (e);
203
+ // Pass absolute path of srcDir because cmake command is run in temp dir
204
+ cmakeArgs.add (srcDir.absolute.path);
205
+ await runCommand ("cmake" , cmakeArgs, tempDir.path);
206
+ await runCommand ("cmake" , ["--build" , "." ], tempDir.path);
207
+ final dllDirUri =
208
+ Platform .isWindows ? tempDir.uri.resolve ("Debug" ) : tempDir.uri;
209
+ final dllDir = Directory .fromUri (dllDirUri);
210
+ for (var entry in dllDir.listSync ()) {
211
+ final dllSuffix = Platform .isWindows ? "dll" : "so" ;
212
+ if (entry.path.endsWith (dllSuffix)) {
213
+ final dllName = entry.uri.pathSegments.last;
214
+ final target = buildDir.uri.resolve (dllName);
215
+ entry.renameSync (target.toFilePath ());
216
+ }
198
217
}
218
+ await tempDir.delete (recursive: true );
199
219
}
0 commit comments