4
4
5
5
//! Configuration for a package.
6
6
7
- use crate :: package:: { Package , PackageOutput } ;
7
+ use crate :: package:: { Package , PackageOutput , PackageSource } ;
8
8
use crate :: target:: Target ;
9
9
use serde_derive:: Deserialize ;
10
10
use std:: collections:: BTreeMap ;
11
11
use std:: path:: Path ;
12
12
use thiserror:: Error ;
13
+ use topological_sort:: TopologicalSort ;
14
+
15
+ /// Describes a set of packages to act upon.
16
+ pub struct PackageMap < ' a > ( BTreeMap < & ' a String , & ' a Package > ) ;
17
+
18
+ // The name of a file which should be created by building a package.
19
+ #[ derive( Clone , Eq , Hash , Ord , PartialEq , PartialOrd ) ]
20
+ struct OutputFile ( String ) ;
21
+
22
+ impl < ' a > PackageMap < ' a > {
23
+ pub fn build_order ( & self ) -> PackageDependencyIter < ' a > {
24
+ let lookup_by_output = self
25
+ . 0
26
+ . iter ( )
27
+ . map ( |( name, package) | ( OutputFile ( package. get_output_file ( name) ) , ( * name, * package) ) )
28
+ . collect :: < BTreeMap < _ , _ > > ( ) ;
29
+
30
+ // Collect all packages, and sort them in dependency order,
31
+ // so we know which ones to build first.
32
+ let mut outputs = TopologicalSort :: < OutputFile > :: new ( ) ;
33
+ for ( package_output, ( _, package) ) in & lookup_by_output {
34
+ match & package. source {
35
+ PackageSource :: Local { .. }
36
+ | PackageSource :: Prebuilt { .. }
37
+ | PackageSource :: Manual => {
38
+ // Skip intermediate leaf packages; if necessary they'll be
39
+ // added to the dependency graph by whatever composite package
40
+ // actually depends on them.
41
+ if !matches ! (
42
+ package. output,
43
+ PackageOutput :: Zone {
44
+ intermediate_only: true
45
+ }
46
+ ) {
47
+ outputs. insert ( package_output. clone ( ) ) ;
48
+ }
49
+ }
50
+ PackageSource :: Composite { packages : deps } => {
51
+ for dep in deps {
52
+ outputs. add_dependency ( OutputFile ( dep. clone ( ) ) , package_output. clone ( ) ) ;
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ PackageDependencyIter {
59
+ lookup_by_output,
60
+ outputs,
61
+ }
62
+ }
63
+ }
64
+
65
+ /// Returns all packages in the order in which they should be built.
66
+ ///
67
+ /// Returns packages in batches that may be built concurrently.
68
+ pub struct PackageDependencyIter < ' a > {
69
+ lookup_by_output : BTreeMap < OutputFile , ( & ' a String , & ' a Package ) > ,
70
+ outputs : TopologicalSort < OutputFile > ,
71
+ }
72
+
73
+ impl < ' a > Iterator for PackageDependencyIter < ' a > {
74
+ type Item = Vec < ( & ' a String , & ' a Package ) > ;
75
+
76
+ fn next ( & mut self ) -> Option < Self :: Item > {
77
+ if self . outputs . is_empty ( ) {
78
+ return None ;
79
+ }
80
+ let batch = self . outputs . pop_all ( ) ;
81
+ assert ! (
82
+ !batch. is_empty( ) || self . outputs. is_empty( ) ,
83
+ "cyclic dependency in package manifest!"
84
+ ) ;
85
+
86
+ Some (
87
+ batch
88
+ . into_iter ( )
89
+ . map ( |output| {
90
+ * self . lookup_by_output . get ( & output) . unwrap_or_else ( || {
91
+ panic ! ( "Could not find a package which creates '{}'" , output. 0 )
92
+ } )
93
+ } )
94
+ . collect ( ) ,
95
+ )
96
+ }
97
+ }
13
98
14
99
/// Describes the configuration for a set of packages.
15
100
#[ derive( Deserialize , Debug ) ]
@@ -21,24 +106,28 @@ pub struct Config {
21
106
22
107
impl Config {
23
108
/// Returns target packages to be assembled on the builder machine.
24
- pub fn packages_to_build ( & self , target : & Target ) -> BTreeMap < & String , & Package > {
25
- self . packages
26
- . iter ( )
27
- . filter ( |( _, pkg) | target. includes_package ( pkg) )
28
- . map ( |( name, pkg) | ( name, pkg) )
29
- . collect ( )
109
+ pub fn packages_to_build ( & self , target : & Target ) -> PackageMap < ' _ > {
110
+ PackageMap (
111
+ self . packages
112
+ . iter ( )
113
+ . filter ( |( _, pkg) | target. includes_package ( pkg) )
114
+ . map ( |( name, pkg) | ( name, pkg) )
115
+ . collect ( ) ,
116
+ )
30
117
}
31
118
32
119
/// Returns target packages which should execute on the deployment machine.
33
- pub fn packages_to_deploy ( & self , target : & Target ) -> BTreeMap < & String , & Package > {
34
- let all_packages = self . packages_to_build ( target) ;
35
- all_packages
36
- . into_iter ( )
37
- . filter ( |( _, pkg) | match pkg. output {
38
- PackageOutput :: Zone { intermediate_only } => !intermediate_only,
39
- PackageOutput :: Tarball => true ,
40
- } )
41
- . collect ( )
120
+ pub fn packages_to_deploy ( & self , target : & Target ) -> PackageMap < ' _ > {
121
+ let all_packages = self . packages_to_build ( target) . 0 ;
122
+ PackageMap (
123
+ all_packages
124
+ . into_iter ( )
125
+ . filter ( |( _, pkg) | match pkg. output {
126
+ PackageOutput :: Zone { intermediate_only } => !intermediate_only,
127
+ PackageOutput :: Tarball => true ,
128
+ } )
129
+ . collect ( ) ,
130
+ )
42
131
}
43
132
}
44
133
@@ -61,3 +150,107 @@ pub fn parse<P: AsRef<Path>>(path: P) -> Result<Config, ParseError> {
61
150
let contents = std:: fs:: read_to_string ( path. as_ref ( ) ) ?;
62
151
parse_manifest ( & contents)
63
152
}
153
+
154
+ #[ cfg( test) ]
155
+ mod test {
156
+ use super :: * ;
157
+
158
+ #[ test]
159
+ fn test_order ( ) {
160
+ let pkg_a_name = String :: from ( "pkg-a" ) ;
161
+ let pkg_a = Package {
162
+ service_name : String :: from ( "a" ) ,
163
+ source : PackageSource :: Manual ,
164
+ output : PackageOutput :: Tarball ,
165
+ only_for_targets : None ,
166
+ setup_hint : None ,
167
+ } ;
168
+
169
+ let pkg_b_name = String :: from ( "pkg-b" ) ;
170
+ let pkg_b = Package {
171
+ service_name : String :: from ( "b" ) ,
172
+ source : PackageSource :: Composite {
173
+ packages : vec ! [ pkg_a. get_output_file( & pkg_a_name) ] ,
174
+ } ,
175
+ output : PackageOutput :: Tarball ,
176
+ only_for_targets : None ,
177
+ setup_hint : None ,
178
+ } ;
179
+
180
+ let cfg = Config {
181
+ packages : BTreeMap :: from ( [
182
+ ( pkg_a_name. clone ( ) , pkg_a. clone ( ) ) ,
183
+ ( pkg_b_name. clone ( ) , pkg_b. clone ( ) ) ,
184
+ ] ) ,
185
+ } ;
186
+
187
+ let mut order = cfg. packages_to_build ( & Target :: default ( ) ) . build_order ( ) ;
188
+ // "pkg-a" comes first, because "pkg-b" depends on it.
189
+ assert_eq ! ( order. next( ) , Some ( vec![ ( & pkg_a_name, & pkg_a) ] ) ) ;
190
+ assert_eq ! ( order. next( ) , Some ( vec![ ( & pkg_b_name, & pkg_b) ] ) ) ;
191
+ }
192
+
193
+ // We're kinda limited by the topological-sort library here, as this is a documented
194
+ // behavior from [TopologicalSort::pop_all].
195
+ //
196
+ // Regardless, test that circular dependencies cause panics.
197
+ #[ test]
198
+ #[ should_panic( expected = "cyclic dependency in package manifest" ) ]
199
+ fn test_cyclic_dependency ( ) {
200
+ let pkg_a_name = String :: from ( "pkg-a" ) ;
201
+ let pkg_b_name = String :: from ( "pkg-b" ) ;
202
+ let pkg_a = Package {
203
+ service_name : String :: from ( "a" ) ,
204
+ source : PackageSource :: Composite {
205
+ packages : vec ! [ String :: from( "pkg-b.tar" ) ] ,
206
+ } ,
207
+ output : PackageOutput :: Tarball ,
208
+ only_for_targets : None ,
209
+ setup_hint : None ,
210
+ } ;
211
+ let pkg_b = Package {
212
+ service_name : String :: from ( "b" ) ,
213
+ source : PackageSource :: Composite {
214
+ packages : vec ! [ String :: from( "pkg-a.tar" ) ] ,
215
+ } ,
216
+ output : PackageOutput :: Tarball ,
217
+ only_for_targets : None ,
218
+ setup_hint : None ,
219
+ } ;
220
+
221
+ let cfg = Config {
222
+ packages : BTreeMap :: from ( [
223
+ ( pkg_a_name. clone ( ) , pkg_a. clone ( ) ) ,
224
+ ( pkg_b_name. clone ( ) , pkg_b. clone ( ) ) ,
225
+ ] ) ,
226
+ } ;
227
+
228
+ let mut order = cfg. packages_to_build ( & Target :: default ( ) ) . build_order ( ) ;
229
+ order. next ( ) ;
230
+ }
231
+
232
+ // Make pkg-a depend on pkg-b.tar, but don't include pkg-b.tar anywhere.
233
+ //
234
+ // Ensure that we see an appropriate panic.
235
+ #[ test]
236
+ #[ should_panic( expected = "Could not find a package which creates 'pkg-b.tar'" ) ]
237
+ fn test_missing_dependency ( ) {
238
+ let pkg_a_name = String :: from ( "pkg-a" ) ;
239
+ let pkg_a = Package {
240
+ service_name : String :: from ( "a" ) ,
241
+ source : PackageSource :: Composite {
242
+ packages : vec ! [ String :: from( "pkg-b.tar" ) ] ,
243
+ } ,
244
+ output : PackageOutput :: Tarball ,
245
+ only_for_targets : None ,
246
+ setup_hint : None ,
247
+ } ;
248
+
249
+ let cfg = Config {
250
+ packages : BTreeMap :: from ( [ ( pkg_a_name. clone ( ) , pkg_a. clone ( ) ) ] ) ,
251
+ } ;
252
+
253
+ let mut order = cfg. packages_to_build ( & Target :: default ( ) ) . build_order ( ) ;
254
+ order. next ( ) ;
255
+ }
256
+ }
0 commit comments