Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/celerity/CelerityResourceMapGenerator.php
12249 views
1
<?php
2
3
final class CelerityResourceMapGenerator extends Phobject {
4
5
private $debug = false;
6
private $resources;
7
8
private $nameMap = array();
9
private $symbolMap = array();
10
private $requiresMap = array();
11
private $packageMap = array();
12
13
public function __construct(CelerityPhysicalResources $resources) {
14
$this->resources = $resources;
15
}
16
17
public function getNameMap() {
18
return $this->nameMap;
19
}
20
21
public function getSymbolMap() {
22
return $this->symbolMap;
23
}
24
25
public function getRequiresMap() {
26
return $this->requiresMap;
27
}
28
29
public function getPackageMap() {
30
return $this->packageMap;
31
}
32
33
public function setDebug($debug) {
34
$this->debug = $debug;
35
return $this;
36
}
37
38
protected function log($message) {
39
if ($this->debug) {
40
$console = PhutilConsole::getConsole();
41
$console->writeErr("%s\n", $message);
42
}
43
}
44
45
public function generate() {
46
$binary_map = $this->rebuildBinaryResources($this->resources);
47
48
$this->log(pht('Found %d binary resources.', count($binary_map)));
49
50
$xformer = id(new CelerityResourceTransformer())
51
->setMinify(false)
52
->setRawURIMap(ipull($binary_map, 'uri'));
53
54
$text_map = $this->rebuildTextResources($this->resources, $xformer);
55
56
$this->log(pht('Found %d text resources.', count($text_map)));
57
58
$resource_graph = array();
59
$requires_map = array();
60
$symbol_map = array();
61
foreach ($text_map as $name => $info) {
62
if (isset($info['provides'])) {
63
$symbol_map[$info['provides']] = $info['hash'];
64
65
// We only need to check for cycles and add this to the requires map
66
// if it actually requires anything.
67
if (!empty($info['requires'])) {
68
$resource_graph[$info['provides']] = $info['requires'];
69
$requires_map[$info['hash']] = $info['requires'];
70
}
71
}
72
}
73
74
$this->detectGraphCycles($resource_graph);
75
$name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash');
76
$hash_map = array_flip($name_map);
77
78
$package_map = $this->rebuildPackages(
79
$this->resources,
80
$symbol_map,
81
$hash_map);
82
83
$this->log(pht('Found %d packages.', count($package_map)));
84
85
$component_map = array();
86
foreach ($package_map as $package_name => $package_info) {
87
foreach ($package_info['symbols'] as $symbol) {
88
$component_map[$symbol] = $package_name;
89
}
90
}
91
92
$name_map = $this->mergeNameMaps(
93
array(
94
array(pht('Binary'), ipull($binary_map, 'hash')),
95
array(pht('Text'), ipull($text_map, 'hash')),
96
array(pht('Package'), ipull($package_map, 'hash')),
97
));
98
$package_map = ipull($package_map, 'symbols');
99
100
ksort($name_map, SORT_STRING);
101
ksort($symbol_map, SORT_STRING);
102
ksort($requires_map, SORT_STRING);
103
ksort($package_map, SORT_STRING);
104
105
$this->nameMap = $name_map;
106
$this->symbolMap = $symbol_map;
107
$this->requiresMap = $requires_map;
108
$this->packageMap = $package_map;
109
110
return $this;
111
}
112
113
public function write() {
114
$map_content = $this->formatMapContent(array(
115
'names' => $this->getNameMap(),
116
'symbols' => $this->getSymbolMap(),
117
'requires' => $this->getRequiresMap(),
118
'packages' => $this->getPackageMap(),
119
));
120
121
$map_path = $this->resources->getPathToMap();
122
$this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path)));
123
Filesystem::writeFile($map_path, $map_content);
124
125
return $this;
126
}
127
128
private function formatMapContent(array $data) {
129
$content = phutil_var_export($data);
130
$generated = '@'.'generated';
131
132
return <<<EOFILE
133
<?php
134
135
/**
136
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
137
*
138
* {$generated}
139
*/
140
return {$content};
141
142
EOFILE;
143
}
144
145
/**
146
* Find binary resources (like PNG and SWF) and return information about
147
* them.
148
*
149
* @param CelerityPhysicalResources Resource map to find binary resources for.
150
* @return map<string, map<string, string>> Resource information map.
151
*/
152
private function rebuildBinaryResources(
153
CelerityPhysicalResources $resources) {
154
155
$binary_map = $resources->findBinaryResources();
156
$result_map = array();
157
158
foreach ($binary_map as $name => $data_hash) {
159
$hash = $this->newResourceHash($data_hash.$name);
160
161
$result_map[$name] = array(
162
'hash' => $hash,
163
'uri' => $resources->getResourceURI($hash, $name),
164
);
165
}
166
167
return $result_map;
168
}
169
170
/**
171
* Find text resources (like JS and CSS) and return information about them.
172
*
173
* @param CelerityPhysicalResources Resource map to find text resources for.
174
* @param CelerityResourceTransformer Configured resource transformer.
175
* @return map<string, map<string, string>> Resource information map.
176
*/
177
private function rebuildTextResources(
178
CelerityPhysicalResources $resources,
179
CelerityResourceTransformer $xformer) {
180
181
$text_map = $resources->findTextResources();
182
$result_map = array();
183
184
foreach ($text_map as $name => $data_hash) {
185
$raw_data = $resources->getResourceData($name);
186
$xformed_data = $xformer->transformResource($name, $raw_data);
187
188
$data_hash = $this->newResourceHash($xformed_data);
189
$hash = $this->newResourceHash($data_hash.$name);
190
191
list($provides, $requires) = $this->getProvidesAndRequires(
192
$name,
193
$raw_data);
194
195
$result_map[$name] = array(
196
'hash' => $hash,
197
);
198
199
if ($provides !== null) {
200
$result_map[$name] += array(
201
'provides' => $provides,
202
'requires' => $requires,
203
);
204
}
205
}
206
207
return $result_map;
208
}
209
210
/**
211
* Parse the `@provides` and `@requires` symbols out of a text resource, like
212
* JS or CSS.
213
*
214
* @param string Resource name.
215
* @param string Resource data.
216
* @return pair<string|null, list<string>|null> The `@provides` symbol and
217
* the list of `@requires` symbols. If the resource is not part of the
218
* dependency graph, both are null.
219
*/
220
private function getProvidesAndRequires($name, $data) {
221
$parser = new PhutilDocblockParser();
222
223
$matches = array();
224
$ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
225
if (!$ok) {
226
throw new Exception(
227
pht(
228
'Resource "%s" does not have a header doc comment. Encode '.
229
'dependency data in a header docblock.',
230
$name));
231
}
232
233
list($description, $metadata) = $parser->parse($matches[0]);
234
235
$provides = $this->parseResourceSymbolList(idx($metadata, 'provides'));
236
$requires = $this->parseResourceSymbolList(idx($metadata, 'requires'));
237
if (!$provides) {
238
// Tests and documentation-only JS is permitted to @provide no targets.
239
return array(null, null);
240
}
241
242
if (count($provides) > 1) {
243
throw new Exception(
244
pht(
245
'Resource "%s" must %s at most one Celerity target.',
246
$name,
247
'@provide'));
248
}
249
250
return array(head($provides), $requires);
251
}
252
253
/**
254
* Check for dependency cycles in the resource graph. Raises an exception if
255
* a cycle is detected.
256
*
257
* @param map<string, list<string>> Map of `@provides` symbols to their
258
* `@requires` symbols.
259
* @return void
260
*/
261
private function detectGraphCycles(array $nodes) {
262
$graph = id(new CelerityResourceGraph())
263
->addNodes($nodes)
264
->setResourceGraph($nodes)
265
->loadGraph();
266
267
foreach ($nodes as $provides => $requires) {
268
$cycle = $graph->detectCycles($provides);
269
if ($cycle) {
270
throw new Exception(
271
pht(
272
'Cycle detected in resource graph: %s',
273
implode(' > ', $cycle)));
274
}
275
}
276
}
277
278
/**
279
* Build package specifications for a given resource source.
280
*
281
* @param CelerityPhysicalResources Resource source to rebuild.
282
* @param map<string, string> Map of `@provides` to hashes.
283
* @param map<string, string> Map of hashes to resource names.
284
* @return map<string, map<string, string>> Package information maps.
285
*/
286
private function rebuildPackages(
287
CelerityPhysicalResources $resources,
288
array $symbol_map,
289
array $reverse_map) {
290
291
$package_map = array();
292
293
$package_spec = $resources->getResourcePackages();
294
foreach ($package_spec as $package_name => $package_symbols) {
295
$type = null;
296
$hashes = array();
297
foreach ($package_symbols as $symbol) {
298
$symbol_hash = idx($symbol_map, $symbol);
299
if ($symbol_hash === null) {
300
throw new Exception(
301
pht(
302
'Package specification for "%s" includes "%s", but that symbol '.
303
'is not %s by any resource.',
304
$package_name,
305
$symbol,
306
'@provided'));
307
}
308
309
$resource_name = $reverse_map[$symbol_hash];
310
$resource_type = $resources->getResourceType($resource_name);
311
if ($type === null) {
312
$type = $resource_type;
313
} else if ($type !== $resource_type) {
314
throw new Exception(
315
pht(
316
'Package specification for "%s" includes resources of multiple '.
317
'types (%s, %s). Each package may only contain one type of '.
318
'resource.',
319
$package_name,
320
$type,
321
$resource_type));
322
}
323
324
$hashes[] = $symbol.':'.$symbol_hash;
325
}
326
327
$hash = $this->newResourceHash(implode("\n", $hashes));
328
$package_map[$package_name] = array(
329
'hash' => $hash,
330
'symbols' => $package_symbols,
331
);
332
}
333
334
return $package_map;
335
}
336
337
private function mergeNameMaps(array $maps) {
338
$result = array();
339
$origin = array();
340
341
foreach ($maps as $map) {
342
list($map_name, $data) = $map;
343
foreach ($data as $name => $hash) {
344
if (empty($result[$name])) {
345
$result[$name] = $hash;
346
$origin[$name] = $map_name;
347
} else {
348
$old = $origin[$name];
349
$new = $map_name;
350
throw new Exception(
351
pht(
352
'Resource source defines two resources with the same name, '.
353
'"%s". One is defined in the "%s" map; the other in the "%s" '.
354
'map. Each resource must have a unique name.',
355
$name,
356
$old,
357
$new));
358
}
359
}
360
}
361
return $result;
362
}
363
364
private function parseResourceSymbolList($list) {
365
if (!$list) {
366
return array();
367
}
368
369
// This is valid:
370
//
371
// @requires x y
372
//
373
// But so is this:
374
//
375
// @requires x
376
// @requires y
377
//
378
// Accept either form and produce a list of symbols.
379
380
$list = (array)$list;
381
382
// We can get `true` values if there was a bare `@requires` in the input.
383
foreach ($list as $key => $item) {
384
if ($item === true) {
385
unset($list[$key]);
386
}
387
}
388
389
$list = implode(' ', $list);
390
$list = trim($list);
391
$list = preg_split('/\s+/', $list);
392
$list = array_filter($list);
393
394
return $list;
395
}
396
397
private function newResourceHash($data) {
398
// This HMAC key is a static, hard-coded value because we don't want the
399
// hashes in the map to depend on database state: when two different
400
// developers regenerate the map, they should end up with the same output.
401
402
$hash = PhabricatorHash::digestHMACSHA256($data, 'celerity-resource-data');
403
404
return substr($hash, 0, 8);
405
}
406
407
}
408
409