1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:file/file.dart' ; |
6 | import 'package:file/local.dart' as local_fs; |
7 | import 'package:meta/meta.dart' ; |
8 | |
9 | import 'common.dart'; |
10 | import 'io.dart'; |
11 | import 'platform.dart'; |
12 | import 'process.dart'; |
13 | import 'signals.dart'; |
14 | |
15 | // package:file/local.dart must not be exported. This exposes LocalFileSystem, |
16 | // which we override to ensure that temporary directories are cleaned up when |
17 | // the tool is killed by a signal. |
18 | export 'package:file/file.dart' ; |
19 | |
20 | /// Exception indicating that a file that was expected to exist was not found. |
21 | class FileNotFoundException implements IOException { |
22 | const FileNotFoundException(this.path); |
23 | |
24 | final String path; |
25 | |
26 | @override |
27 | String toString() => 'File not found: $path' ; |
28 | } |
29 | |
30 | /// Various convenience file system methods. |
31 | class FileSystemUtils { |
32 | FileSystemUtils({ |
33 | required FileSystem fileSystem, |
34 | required Platform platform, |
35 | }) : _fileSystem = fileSystem, |
36 | _platform = platform; |
37 | |
38 | final FileSystem _fileSystem; |
39 | |
40 | final Platform _platform; |
41 | |
42 | /// Appends a number to a filename in order to make it unique under a |
43 | /// directory. |
44 | File getUniqueFile(Directory dir, String baseName, String ext) { |
45 | return _getUniqueFile(dir, baseName, ext); |
46 | } |
47 | |
48 | /// Appends a number to a directory name in order to make it unique under a |
49 | /// directory. |
50 | Directory getUniqueDirectory(Directory dir, String baseName) { |
51 | final FileSystem fs = dir.fileSystem; |
52 | int i = 1; |
53 | |
54 | while (true) { |
55 | final String name = ' ${baseName}_ ${i.toString().padLeft(2, '0' )}' ; |
56 | final Directory directory = fs.directory(_fileSystem.path.join(dir.path, name)); |
57 | if (!directory.existsSync()) { |
58 | return directory; |
59 | } |
60 | i += 1; |
61 | } |
62 | } |
63 | |
64 | /// Escapes [path]. |
65 | /// |
66 | /// On Windows it replaces all '\' with '\\'. On other platforms, it returns the |
67 | /// path unchanged. |
68 | String escapePath(String path) => _platform.isWindows ? path.replaceAll(r'\' , r'\\' ) : path; |
69 | |
70 | /// Returns true if the file system [entity] has not been modified since the |
71 | /// latest modification to [referenceFile]. |
72 | /// |
73 | /// Returns true, if [entity] does not exist. |
74 | /// |
75 | /// Returns false, if [entity] exists, but [referenceFile] does not. |
76 | bool isOlderThanReference({ |
77 | required FileSystemEntity entity, |
78 | required File referenceFile, |
79 | }) { |
80 | if (!entity.existsSync()) { |
81 | return true; |
82 | } |
83 | return referenceFile.existsSync() |
84 | && referenceFile.statSync().modified.isAfter(entity.statSync().modified); |
85 | } |
86 | |
87 | /// Return the absolute path of the user's home directory. |
88 | String? get homeDirPath { |
89 | String? path = _platform.isWindows |
90 | ? _platform.environment['USERPROFILE' ] |
91 | : _platform.environment['HOME' ]; |
92 | if (path != null) { |
93 | path = _fileSystem.path.absolute(path); |
94 | } |
95 | return path; |
96 | } |
97 | } |
98 | |
99 | /// Return a relative path if [fullPath] is contained by the cwd, else return an |
100 | /// absolute path. |
101 | String getDisplayPath(String fullPath, FileSystem fileSystem) { |
102 | final String cwd = fileSystem.currentDirectory.path + fileSystem.path.separator; |
103 | return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath; |
104 | } |
105 | |
106 | /// Creates `destDir` if needed, then recursively copies `srcDir` to |
107 | /// `destDir`, invoking [onFileCopied], if specified, for each |
108 | /// source/destination file pair. |
109 | /// |
110 | /// Skips files if [shouldCopyFile] returns `false`. |
111 | /// Does not recurse over directories if [shouldCopyDirectory] returns `false`. |
112 | /// |
113 | /// If [followLinks] is false, then any symbolic links found are reported as |
114 | /// [Link] objects, rather than as directories or files, and are not recursed into. |
115 | /// |
116 | /// If [followLinks] is true, then working links are reported as directories or |
117 | /// files, depending on what they point to. |
118 | void copyDirectory( |
119 | Directory srcDir, |
120 | Directory destDir, { |
121 | bool Function(File srcFile, File destFile)? shouldCopyFile, |
122 | bool Function(Directory)? shouldCopyDirectory, |
123 | void Function(File srcFile, File destFile)? onFileCopied, |
124 | bool followLinks = true, |
125 | }) { |
126 | if (!srcDir.existsSync()) { |
127 | throw Exception('Source directory " ${srcDir.path}" does not exist, nothing to copy' ); |
128 | } |
129 | |
130 | if (!destDir.existsSync()) { |
131 | destDir.createSync(recursive: true); |
132 | } |
133 | |
134 | for (final FileSystemEntity entity in srcDir.listSync(followLinks: followLinks)) { |
135 | final String newPath = destDir.fileSystem.path.join(destDir.path, entity.basename); |
136 | if (entity is Link) { |
137 | final Link newLink = destDir.fileSystem.link(newPath); |
138 | newLink.createSync(entity.targetSync()); |
139 | } else if (entity is File) { |
140 | final File newFile = destDir.fileSystem.file(newPath); |
141 | if (shouldCopyFile != null && !shouldCopyFile(entity, newFile)) { |
142 | continue; |
143 | } |
144 | newFile.writeAsBytesSync(entity.readAsBytesSync()); |
145 | onFileCopied?.call(entity, newFile); |
146 | } else if (entity is Directory) { |
147 | if (shouldCopyDirectory != null && !shouldCopyDirectory(entity)) { |
148 | continue; |
149 | } |
150 | copyDirectory( |
151 | entity, |
152 | destDir.fileSystem.directory(newPath), |
153 | shouldCopyFile: shouldCopyFile, |
154 | onFileCopied: onFileCopied, |
155 | followLinks: followLinks, |
156 | ); |
157 | } else { |
158 | throw Exception(' ${entity.path} is neither File nor Directory, was ${entity.runtimeType}' ); |
159 | } |
160 | } |
161 | } |
162 | |
163 | File _getUniqueFile(Directory dir, String baseName, String ext) { |
164 | final FileSystem fs = dir.fileSystem; |
165 | int i = 1; |
166 | |
167 | while (true) { |
168 | final String name = ' ${baseName}_ ${i.toString().padLeft(2, '0' )}. $ext' ; |
169 | final File file = fs.file(dir.fileSystem.path.join(dir.path, name)); |
170 | if (!file.existsSync()) { |
171 | file.createSync(recursive: true); |
172 | return file; |
173 | } |
174 | i += 1; |
175 | } |
176 | } |
177 | |
178 | /// Appends a number to a filename in order to make it unique under a |
179 | /// directory. |
180 | File getUniqueFile(Directory dir, String baseName, String ext) { |
181 | return _getUniqueFile(dir, baseName, ext); |
182 | } |
183 | |
184 | /// This class extends [local_fs.LocalFileSystem] in order to clean up |
185 | /// directories and files that the tool creates under the system temporary |
186 | /// directory when the tool exits either normally or when killed by a signal. |
187 | class LocalFileSystem extends local_fs.LocalFileSystem { |
188 | LocalFileSystem(this._signals, this._fatalSignals, this.shutdownHooks); |
189 | |
190 | @visibleForTesting |
191 | LocalFileSystem.test({ |
192 | required Signals signals, |
193 | List<ProcessSignal> fatalSignals = Signals.defaultExitSignals, |
194 | }) : this(signals, fatalSignals, ShutdownHooks()); |
195 | |
196 | Directory? _systemTemp; |
197 | final Map<ProcessSignal, Object> _signalTokens = <ProcessSignal, Object>{}; |
198 | |
199 | final ShutdownHooks shutdownHooks; |
200 | |
201 | Future<void> dispose() async { |
202 | _tryToDeleteTemp(); |
203 | for (final MapEntry<ProcessSignal, Object> signalToken in _signalTokens.entries) { |
204 | await _signals.removeHandler(signalToken.key, signalToken.value); |
205 | } |
206 | _signalTokens.clear(); |
207 | } |
208 | |
209 | final Signals _signals; |
210 | final List<ProcessSignal> _fatalSignals; |
211 | |
212 | void _tryToDeleteTemp() { |
213 | try { |
214 | if (_systemTemp?.existsSync() ?? false) { |
215 | _systemTemp?.deleteSync(recursive: true); |
216 | } |
217 | } on FileSystemException { |
218 | // ignore |
219 | } |
220 | _systemTemp = null; |
221 | } |
222 | |
223 | // This getter returns a fresh entry under /tmp, like |
224 | // /tmp/flutter_tools.abcxyz, then the rest of the tool creates /tmp entries |
225 | // under that, like /tmp/flutter_tools.abcxyz/flutter_build_stuff.123456. |
226 | // Right before exiting because of a signal or otherwise, we delete |
227 | // /tmp/flutter_tools.abcxyz, not the whole of /tmp. |
228 | @override |
229 | Directory get systemTempDirectory { |
230 | if (_systemTemp == null) { |
231 | if (!superSystemTempDirectory.existsSync()) { |
232 | throwToolExit('Your system temp directory ( ${superSystemTempDirectory.path}) does not exist. ' |
233 | 'Did you set an invalid override in your environment? See issue https://github.com/flutter/flutter/issues/74042 for more context.' |
234 | ); |
235 | } |
236 | _systemTemp = superSystemTempDirectory.createTempSync('flutter_tools.' ) |
237 | ..createSync(recursive: true); |
238 | // Make sure that the temporary directory is cleaned up if the tool is |
239 | // killed by a signal. |
240 | for (final ProcessSignal signal in _fatalSignals) { |
241 | final Object token = _signals.addHandler( |
242 | signal, |
243 | (ProcessSignal _) { |
244 | _tryToDeleteTemp(); |
245 | }, |
246 | ); |
247 | _signalTokens[signal] = token; |
248 | } |
249 | // Make sure that the temporary directory is cleaned up when the tool |
250 | // exits normally. |
251 | shutdownHooks.addShutdownHook( |
252 | _tryToDeleteTemp, |
253 | ); |
254 | } |
255 | return _systemTemp!; |
256 | } |
257 | |
258 | // This only exist because the memory file system does not support a systemTemp that does not exists #74042 |
259 | @visibleForTesting |
260 | Directory get superSystemTempDirectory => super.systemTempDirectory; |
261 | } |
262 | |