Skip to content

Commit 30eb18c

Browse files
committed
Give error if globall activated package depends on hooks
1 parent 81409a1 commit 30eb18c

File tree

4 files changed

+140
-26
lines changed

4 files changed

+140
-26
lines changed

lib/src/global_packages.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ class GlobalPackages {
192192

193193
// Get the package's dependencies.
194194
await entrypoint.acquireDependencies(SolveType.get);
195+
for (final package in (await entrypoint.packageGraph)
196+
.transitiveDependencies(entrypoint.workPackage.name)) {
197+
if (fileExists(p.join(package.dir, 'hooks', 'build.dart'))) {
198+
fail('Cannot `global activate` packages with hooks.');
199+
}
200+
}
195201
final activatedPackage = entrypoint.workPackage;
196202
final name = activatedPackage.name;
197203
_describeActive(name, cache);
@@ -260,10 +266,24 @@ class GlobalPackages {
260266
}
261267
rethrow;
262268
}
269+
263270
// We want the entrypoint to be rooted at 'dep' not the dummy-package.
264271
result.packages.removeWhere((id) => id.name == 'pub global activate');
265272

266273
final lockFile = await result.downloadCachedPackages(cache);
274+
275+
// Because we know that the dummy package never is a workspace we can
276+
// iterate all packages.
277+
// TODO(sigurdm): refactor PackageGraph to make it possible to query without
278+
// loading entrypoint.
279+
for (final package in result.packages) {
280+
if (fileExists(
281+
p.join(cache.getDirectory(package), 'hooks', 'build.dart'),
282+
)) {
283+
fail('Cannot `global activate` packages with hooks.');
284+
}
285+
}
286+
267287
final sameVersions =
268288
originalLockFile != null && originalLockFile.samePackageIds(lockFile);
269289

lib/src/package_graph.dart

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'package:graphs/graphs.dart';
6-
75
import 'entrypoint.dart';
86
import 'package.dart';
97
import 'solver.dart';
108
import 'source/cached.dart';
119
import 'source/sdk.dart';
12-
import 'utils.dart';
1310

1411
/// A holistic view of the entire transitive dependency graph for an entrypoint.
1512
class PackageGraph {
@@ -23,9 +20,6 @@ class PackageGraph {
2320
/// relevant in the current context.
2421
final Map<String, Package> packages;
2522

26-
/// A map of transitive dependencies for each package.
27-
Map<String, Set<Package>>? _transitiveDependencies;
28-
2923
PackageGraph(this.entrypoint, this.packages);
3024

3125
/// Creates a package graph using the data from [result].
@@ -56,27 +50,21 @@ class PackageGraph {
5650
/// dev and override. For any other package, it ignores dev and override
5751
/// dependencies.
5852
Set<Package> transitiveDependencies(String package) {
59-
if (package == entrypoint.workspaceRoot.name) {
60-
return packages.values.toSet();
61-
}
53+
final result = <Package>{};
6254

63-
if (_transitiveDependencies == null) {
64-
final graph = mapMap<String, Package, String, Iterable<String>>(
65-
packages,
66-
value: (_, package) => package.dependencies.keys,
67-
);
68-
final closure = transitiveClosure(graph.keys, (n) => graph[n]!);
69-
_transitiveDependencies =
70-
mapMap<String, Set<String>, String, Set<Package>>(
71-
closure,
72-
value: (depender, names) {
73-
final set = names.map((name) => packages[name]!).toSet();
74-
set.add(packages[depender]!);
75-
return set;
76-
},
77-
);
55+
final stack = [package];
56+
final visited = <String>{};
57+
while (stack.isNotEmpty) {
58+
final current = stack.removeLast();
59+
if (!visited.add(current)) continue;
60+
final currentPackage = packages[current]!;
61+
result.add(currentPackage);
62+
stack.addAll(currentPackage.dependencies.keys);
63+
if (current == package) {
64+
stack.addAll(currentPackage.devDependencies.keys);
65+
}
7866
}
79-
return _transitiveDependencies![package]!;
67+
return result;
8068
}
8169

8270
bool _isPackageFromImmutableSource(String package) {

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,4 +474,4 @@ packages:
474474
source: hosted
475475
version: "2.2.2"
476476
sdks:
477-
dart: ">=3.7.0 <4.0.0"
477+
dart: ">=3.8.0 <4.0.0"
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:path/path.dart' as p;
6+
import 'package:test/test.dart';
7+
8+
import '../../descriptor.dart';
9+
import '../../test_pub.dart';
10+
11+
void main() {
12+
test(
13+
'activating a package from path gives error if package uses hooks',
14+
() async {
15+
final server = await servePackages();
16+
server.serve(
17+
'hooks',
18+
'1.0.0',
19+
contents: [
20+
dir('hooks', [file('build.dart')]),
21+
],
22+
);
23+
server.serve('no_hooks', '1.0.0');
24+
25+
await dir(appPath, [
26+
libPubspec(
27+
'foo',
28+
'1.2.3',
29+
extras: {
30+
'workspace': ['pkgs/foo_hooks', 'pkgs/foo_no_hooks'],
31+
},
32+
sdk: '^3.5.0',
33+
),
34+
dir('pkgs', [
35+
dir('foo_hooks', [
36+
libPubspec(
37+
'foo_hooks',
38+
'1.1.1',
39+
devDeps: {'hooks': '^1.0.0'},
40+
resolutionWorkspace: true,
41+
),
42+
]),
43+
dir('foo_no_hooks', [
44+
libPubspec(
45+
'foo_no_hooks',
46+
'1.1.1',
47+
deps: {'no_hooks': '^1.0.0'},
48+
resolutionWorkspace: true,
49+
),
50+
]),
51+
]),
52+
]).create();
53+
54+
await runPub(
55+
args: ['global', 'activate', '-spath', p.join('pkgs', 'foo_hooks')],
56+
environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
57+
error: 'Cannot `global activate` packages with hooks.',
58+
exitCode: 1,
59+
);
60+
61+
await runPub(
62+
args: ['global', 'activate', '-spath', p.join('pkgs', 'foo_no_hooks')],
63+
environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
64+
);
65+
},
66+
);
67+
68+
test('activating a hosted package gives error if package uses hooks in any'
69+
' dependency', () async {
70+
final server = await servePackages();
71+
server.serve(
72+
'hooks',
73+
'1.0.0',
74+
contents: [
75+
dir('hooks', [file('build.dart')]),
76+
],
77+
);
78+
server.serve('foo_hooks', '1.1.1', deps: {'hooks': '^1.0.0'});
79+
server.serve(
80+
'foo_hooks_in_dev_deps',
81+
'1.0.0',
82+
pubspec: {
83+
'dev_dependencies': {'hooks': '^1.0.0'},
84+
},
85+
);
86+
87+
await runPub(
88+
args: ['global', 'activate', 'hooks'],
89+
environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
90+
error: 'Cannot `global activate` packages with hooks.',
91+
exitCode: 1,
92+
);
93+
94+
await runPub(
95+
args: ['global', 'activate', 'foo_hooks'],
96+
environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
97+
error: 'Cannot `global activate` packages with hooks.',
98+
exitCode: 1,
99+
);
100+
101+
await runPub(
102+
args: ['global', 'activate', 'foo_hooks_in_dev_deps'],
103+
environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
104+
);
105+
});
106+
}

0 commit comments

Comments
 (0)