Skip to content

Commit 0de37a4

Browse files
committed
Use glob for more efficient file operations
1 parent 792379d commit 0de37a4

File tree

4 files changed

+123
-83
lines changed

4 files changed

+123
-83
lines changed

src/serious_python/bin/package_command.dart

Lines changed: 72 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'dart:io';
44
import 'package:archive/archive_io.dart';
55
import 'package:args/command_runner.dart';
66
import 'package:crypto/crypto.dart';
7+
import 'package:glob/glob.dart';
8+
import 'package:glob/list_local_fs.dart';
79
import 'package:http/http.dart' as http;
810
import 'package:path/path.dart' as path;
911
import 'package:shelf/shelf.dart';
@@ -58,11 +60,24 @@ const platforms = {
5860
}
5961
};
6062

61-
const junkFileExtensionsDesktop = [".c", ".h", ".hpp", ".typed", ".a", ".pdb"];
62-
const junkFileExtensionsMobile = [...junkFileExtensionsDesktop, ".exe", ".dll"];
63-
64-
const junkFilesDesktop = ["__pycache__"];
65-
const junkFilesMobile = [...junkFilesDesktop, "bin"];
63+
final dartFile = Glob("**.dart");
64+
65+
const junkFilesDesktop = [
66+
"**.c",
67+
"**.h",
68+
"**.cpp",
69+
"**.hpp",
70+
"**.typed",
71+
"**.a",
72+
"**.pdb",
73+
"**.pyi",
74+
"**.pxd",
75+
"**.pyx",
76+
"**/LICENSE",
77+
"**.dist-info",
78+
"**/__pycache__",
79+
];
80+
const junkFilesMobile = [...junkFilesDesktop, "**.exe", "**.dll", "**/bin"];
6681

6782
class PackageCommand extends Command {
6883
bool _verbose = false;
@@ -104,6 +119,16 @@ class PackageCommand extends Command {
104119
help:
105120
"Cleanup app and packages from unneccessary files and directories.",
106121
negatable: false);
122+
argParser.addFlag("cleanup-app",
123+
help: "Cleanup app from unneccessary files and directories.",
124+
negatable: false);
125+
argParser.addMultiOption('cleanup-app-files',
126+
help: "List of globs to delete extra app files and directories.");
127+
argParser.addFlag("cleanup-packages",
128+
help: "Cleanup packages from unneccessary files and directories.",
129+
negatable: false);
130+
argParser.addMultiOption('cleanup-packages-files',
131+
help: "List of globs to delete extra packages files and directories.");
107132
argParser.addFlag("verbose", help: "Verbose output.", negatable: false);
108133
}
109134

@@ -135,6 +160,10 @@ class PackageCommand extends Command {
135160
bool compileApp = argResults?["compile-app"];
136161
bool compilePackages = argResults?["compile-packages"];
137162
bool cleanup = argResults?["cleanup"];
163+
bool cleanupApp = argResults?["cleanup-app"];
164+
List<String> cleanupAppFiles = argResults?['cleanup-app-files'];
165+
bool cleanupPackages = argResults?["cleanup-packages"];
166+
List<String> cleanupPackagesFiles = argResults?['cleanup-packages-files'];
138167
_verbose = argResults?["verbose"];
139168

140169
if (path.isRelative(sourceDirPath)) {
@@ -162,8 +191,6 @@ class PackageCommand extends Command {
162191
bool isMobile = (platform == "iOS" || platform == "Android");
163192
bool isWeb = platform == "Pyodide";
164193

165-
var junkFileExtensions =
166-
isMobile ? junkFileExtensionsMobile : junkFileExtensionsDesktop;
167194
var junkFiles = isMobile ? junkFilesMobile : junkFilesDesktop;
168195

169196
// Extra indexs
@@ -212,19 +239,19 @@ class PackageCommand extends Command {
212239
await runPython(['-m', 'compileall', '-b', tempDir.path]);
213240

214241
verbose("Deleting original .py files");
215-
await cleanupPyPackages(tempDir, [".py"], []);
242+
await cleanupDir(tempDir, ["**.py"]);
216243
}
217244

218245
// cleanup
219-
if (cleanup) {
246+
if (cleanupApp || cleanup) {
247+
var allJunkFiles = [...junkFiles, ...cleanupAppFiles];
220248
if (_verbose) {
221249
verbose(
222-
"Delete unnecessary app files with extensions: $junkFileExtensions");
223-
verbose("Delete unnecessary app files and directories: $junkFiles");
250+
"Delete unnecessary app files and directories: $allJunkFiles");
224251
} else {
225252
stdout.writeln(("Cleanup app"));
226253
}
227-
await cleanupPyPackages(tempDir, junkFileExtensions, junkFiles);
254+
await cleanupDir(tempDir, allJunkFiles);
228255
}
229256

230257
// install requirements
@@ -358,21 +385,19 @@ class PackageCommand extends Command {
358385
await runPython(['-m', 'compileall', '-b', sitePackagesDir]);
359386

360387
verbose("Deleting original .py files");
361-
await cleanupPyPackages(Directory(sitePackagesDir), [".py"], []);
388+
await cleanupDir(Directory(sitePackagesDir), ["**.py"]);
362389
}
363390

364391
// cleanup packages
365-
if (cleanup) {
392+
if (cleanupPackages || cleanup) {
393+
var allJunkFiles = [...junkFiles, ...cleanupPackagesFiles];
366394
if (_verbose) {
367395
verbose(
368-
"Delete unnecessary package files with extensions: $junkFileExtensions");
369-
verbose(
370-
"Delete unnecessary package files and directories: $junkFiles");
396+
"Delete unnecessary package files and directories: $allJunkFiles");
371397
} else {
372398
stdout.writeln(("Cleanup installed packages"));
373399
}
374-
await cleanupPyPackages(
375-
Directory(sitePackagesDir), junkFileExtensions, junkFiles);
400+
await cleanupDir(Directory(sitePackagesDir), allJunkFiles);
376401
}
377402
} finally {
378403
if (sitecustomizeDir != null && await sitecustomizeDir.exists()) {
@@ -435,26 +460,20 @@ class PackageCommand extends Command {
435460
}
436461
}
437462

438-
Future<void> cleanupPyPackages(Directory directory,
439-
List<String> fileExtensions, List<String> filesAndDirectories) async {
440-
await for (var entity in directory.list()) {
441-
if (entity is Directory) {
442-
await cleanupPyPackages(entity, fileExtensions, filesAndDirectories);
443-
} else if (entity is File &&
444-
(fileExtensions.contains(path.extension(entity.path)) ||
445-
filesAndDirectories.contains(path.basename(entity.path)))) {
446-
verbose("Deleting ${entity.path}");
447-
448-
await entity.delete();
449-
}
450-
}
463+
Future<void> cleanupDir(Directory directory, List<String> filesGlobs) async {
464+
return cleanupDirRecursive(
465+
directory, filesGlobs.map((g) => Glob(g.replaceAll("\\", "/"))));
466+
}
451467

452-
await for (var entity in directory.list()) {
453-
if (entity is Directory &&
454-
filesAndDirectories.contains(path.basename(entity.path))) {
468+
Future<void> cleanupDirRecursive(
469+
Directory directory, Iterable<Glob> globs) async {
470+
for (var entity in directory.listSync()) {
471+
if (globs.any((g) => g.matches(entity.path.replaceAll("\\", "/"))) &&
472+
await entity.exists()) {
455473
verbose("Deleting ${entity.path}");
456-
457474
await entity.delete(recursive: true);
475+
} else if (entity is Directory) {
476+
await cleanupDirRecursive(entity, globs);
458477
}
459478
}
460479
}
@@ -533,7 +552,7 @@ class PackageCommand extends Command {
533552
'tar', ['-xzf', pythonArchivePath, '-C', _pythonDir!.path]);
534553

535554
if (Platform.isMacOS) {
536-
copySysconfigFiles(_pythonDir!.path);
555+
duplicateSysconfigFile(_pythonDir!.path);
537556
}
538557
}
539558
}
@@ -547,53 +566,23 @@ class PackageCommand extends Command {
547566
return await runExec(pythonExePath, args, environment: environment);
548567
}
549568

550-
void copySysconfigFiles(String pythonDir) {
551-
final libPath = Directory(path.join(pythonDir, "python", "lib"));
552-
553-
// Find the Python version dynamically (e.g., python3.10, python3.11)
554-
if (!libPath.existsSync()) {
555-
stderr.writeln('Python lib directory not found: $libPath');
556-
exit(1);
557-
}
558-
559-
// Find the actual Python 3.x subdirectory
560-
final pythonSubDir = libPath
561-
.listSync()
562-
.whereType<Directory>()
563-
.firstWhere((dir) => RegExp(r'python3\.\d+').hasMatch(dir.path),
564-
orElse: () => throw Exception('No Python 3.x directory found'))
565-
.path;
566-
567-
final targetDir = Directory(pythonSubDir);
568-
569-
// Search for `_sysconfigdata__*.py` files
570-
final files = targetDir
571-
.listSync()
572-
.whereType<File>()
573-
.where((file) => RegExp(r'_sysconfigdata__.*\.py$').hasMatch(file.path))
574-
.toList();
575-
576-
if (files.isEmpty) {
577-
stderr.writeln('No matching _sysconfigdata__ files found in $targetDir');
578-
exit(1);
579-
}
580-
581-
for (final file in files) {
582-
final dir = file.parent.path;
583-
584-
// Define the new filenames
585-
final targets = [
586-
'_sysconfigdata__darwin_arm64_iphoneos.py',
587-
'_sysconfigdata__darwin_arm64_iphonesimulator.py',
588-
'_sysconfigdata__darwin_x86_64_iphonesimulator.py',
589-
];
590-
591-
for (final target in targets) {
592-
final targetPath = '$dir/$target';
593-
file.copySync(targetPath);
594-
if (_verbose) {
595-
verbose('Copied ${file.path} -> $targetPath');
569+
void duplicateSysconfigFile(String pythonDir) {
570+
final sysConfigGlob = Glob("python/lib/python3.*/_sysconfigdata__*.py");
571+
for (var sysConfig in sysConfigGlob.listSync(root: pythonDir)) {
572+
// copy the first found sys config and exit
573+
if (sysConfig is File) {
574+
for (final target in [
575+
'_sysconfigdata__darwin_arm64_iphoneos.py',
576+
'_sysconfigdata__darwin_arm64_iphonesimulator.py',
577+
'_sysconfigdata__darwin_x86_64_iphonesimulator.py',
578+
]) {
579+
var targetPath = path.join(sysConfig.parent.path, target);
580+
(sysConfig as File).copySync(targetPath);
581+
if (_verbose) {
582+
verbose('Copied ${sysConfig.path} -> $targetPath');
583+
}
596584
}
585+
break;
597586
}
598587
}
599588
}

src/serious_python/example/run_example/pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ packages:
125125
description: flutter
126126
source: sdk
127127
version: "0.0.0"
128+
glob:
129+
dependency: transitive
130+
description:
131+
name: glob
132+
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
133+
url: "https://pub.dev"
134+
source: hosted
135+
version: "2.1.3"
128136
http:
129137
dependency: transitive
130138
description:

src/serious_python/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ dependencies:
5151
http: ^1.2.1
5252
shelf: ^1.4.1
5353
crypto: ^3.0.5
54+
glob: ^2.1.3
5455

5556
dev_dependencies:
5657
flutter_test:
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:glob/glob.dart';
3+
4+
void main() {
5+
test("test globs", () async {
6+
final dartFile = Glob("**.dart");
7+
expect(dartFile.matches("a\\b\\c\\something.dart"), true);
8+
expect(dartFile.matches("a/something.dart/sub-dir"), false);
9+
10+
final distInfo = Glob("**.dist-info");
11+
expect(distInfo.matches("lru_dict-1.3.0.dist-info"), true);
12+
expect(distInfo.matches("a/b/lru_dict-1.3.0.dist-info"), true);
13+
expect(distInfo.matches("lru_dict-1.3.0.dist-info/METADATA"), false);
14+
15+
final pyCache = Glob("**\\__pycache__");
16+
expect(pyCache.matches("a/b/__pycache__"), true);
17+
expect(pyCache.matches("a\\__pycache__"), true);
18+
expect(pyCache.matches("__pycache__"), true);
19+
expect(pyCache.matches("a/__pycache__/b"), false);
20+
expect(pyCache.matches("a/__pycache"), false);
21+
22+
final pyCache2 = Glob("**/__pycache__");
23+
expect(pyCache2.matches("c:/aaa/bbb/__pycache__"), true);
24+
expect(pyCache2.matches("a/b/__pycache__"), true);
25+
26+
final binPyCache = Glob("**/bin/__pycache__");
27+
expect(binPyCache.matches("a/b/bin/__pycache__"), true);
28+
29+
final numpyTests = Glob("**/numpy/**/tests");
30+
expect(numpyTests.matches("a/b/numpy/typing/tests"), true);
31+
expect(numpyTests.matches("a/b/numpy/random/tests"), true);
32+
expect(numpyTests.matches("a/b/numpy/something/else/tests"), true);
33+
expect(numpyTests.matches("a/b/package_a/typing/tests"), false);
34+
35+
final numpyTests2 = Glob("**/numpy/tests");
36+
expect(numpyTests2.matches("a/b/numpy/tests"), true);
37+
expect(numpyTests2.matches("a/b/numpy/abc/tests"), false);
38+
39+
final numpyCoreLib = Glob("**/numpy/_core/lib");
40+
expect(numpyCoreLib.matches("a/b/numpy/_core/lib"), true);
41+
});
42+
}

0 commit comments

Comments
 (0)