Path: blob/master/src/applications/celerity/CelerityResourceMapGenerator.php
12249 views
<?php12final class CelerityResourceMapGenerator extends Phobject {34private $debug = false;5private $resources;67private $nameMap = array();8private $symbolMap = array();9private $requiresMap = array();10private $packageMap = array();1112public function __construct(CelerityPhysicalResources $resources) {13$this->resources = $resources;14}1516public function getNameMap() {17return $this->nameMap;18}1920public function getSymbolMap() {21return $this->symbolMap;22}2324public function getRequiresMap() {25return $this->requiresMap;26}2728public function getPackageMap() {29return $this->packageMap;30}3132public function setDebug($debug) {33$this->debug = $debug;34return $this;35}3637protected function log($message) {38if ($this->debug) {39$console = PhutilConsole::getConsole();40$console->writeErr("%s\n", $message);41}42}4344public function generate() {45$binary_map = $this->rebuildBinaryResources($this->resources);4647$this->log(pht('Found %d binary resources.', count($binary_map)));4849$xformer = id(new CelerityResourceTransformer())50->setMinify(false)51->setRawURIMap(ipull($binary_map, 'uri'));5253$text_map = $this->rebuildTextResources($this->resources, $xformer);5455$this->log(pht('Found %d text resources.', count($text_map)));5657$resource_graph = array();58$requires_map = array();59$symbol_map = array();60foreach ($text_map as $name => $info) {61if (isset($info['provides'])) {62$symbol_map[$info['provides']] = $info['hash'];6364// We only need to check for cycles and add this to the requires map65// if it actually requires anything.66if (!empty($info['requires'])) {67$resource_graph[$info['provides']] = $info['requires'];68$requires_map[$info['hash']] = $info['requires'];69}70}71}7273$this->detectGraphCycles($resource_graph);74$name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash');75$hash_map = array_flip($name_map);7677$package_map = $this->rebuildPackages(78$this->resources,79$symbol_map,80$hash_map);8182$this->log(pht('Found %d packages.', count($package_map)));8384$component_map = array();85foreach ($package_map as $package_name => $package_info) {86foreach ($package_info['symbols'] as $symbol) {87$component_map[$symbol] = $package_name;88}89}9091$name_map = $this->mergeNameMaps(92array(93array(pht('Binary'), ipull($binary_map, 'hash')),94array(pht('Text'), ipull($text_map, 'hash')),95array(pht('Package'), ipull($package_map, 'hash')),96));97$package_map = ipull($package_map, 'symbols');9899ksort($name_map, SORT_STRING);100ksort($symbol_map, SORT_STRING);101ksort($requires_map, SORT_STRING);102ksort($package_map, SORT_STRING);103104$this->nameMap = $name_map;105$this->symbolMap = $symbol_map;106$this->requiresMap = $requires_map;107$this->packageMap = $package_map;108109return $this;110}111112public function write() {113$map_content = $this->formatMapContent(array(114'names' => $this->getNameMap(),115'symbols' => $this->getSymbolMap(),116'requires' => $this->getRequiresMap(),117'packages' => $this->getPackageMap(),118));119120$map_path = $this->resources->getPathToMap();121$this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path)));122Filesystem::writeFile($map_path, $map_content);123124return $this;125}126127private function formatMapContent(array $data) {128$content = phutil_var_export($data);129$generated = '@'.'generated';130131return <<<EOFILE132<?php133134/**135* This file is automatically generated. Use 'bin/celerity map' to rebuild it.136*137* {$generated}138*/139return {$content};140141EOFILE;142}143144/**145* Find binary resources (like PNG and SWF) and return information about146* them.147*148* @param CelerityPhysicalResources Resource map to find binary resources for.149* @return map<string, map<string, string>> Resource information map.150*/151private function rebuildBinaryResources(152CelerityPhysicalResources $resources) {153154$binary_map = $resources->findBinaryResources();155$result_map = array();156157foreach ($binary_map as $name => $data_hash) {158$hash = $this->newResourceHash($data_hash.$name);159160$result_map[$name] = array(161'hash' => $hash,162'uri' => $resources->getResourceURI($hash, $name),163);164}165166return $result_map;167}168169/**170* Find text resources (like JS and CSS) and return information about them.171*172* @param CelerityPhysicalResources Resource map to find text resources for.173* @param CelerityResourceTransformer Configured resource transformer.174* @return map<string, map<string, string>> Resource information map.175*/176private function rebuildTextResources(177CelerityPhysicalResources $resources,178CelerityResourceTransformer $xformer) {179180$text_map = $resources->findTextResources();181$result_map = array();182183foreach ($text_map as $name => $data_hash) {184$raw_data = $resources->getResourceData($name);185$xformed_data = $xformer->transformResource($name, $raw_data);186187$data_hash = $this->newResourceHash($xformed_data);188$hash = $this->newResourceHash($data_hash.$name);189190list($provides, $requires) = $this->getProvidesAndRequires(191$name,192$raw_data);193194$result_map[$name] = array(195'hash' => $hash,196);197198if ($provides !== null) {199$result_map[$name] += array(200'provides' => $provides,201'requires' => $requires,202);203}204}205206return $result_map;207}208209/**210* Parse the `@provides` and `@requires` symbols out of a text resource, like211* JS or CSS.212*213* @param string Resource name.214* @param string Resource data.215* @return pair<string|null, list<string>|null> The `@provides` symbol and216* the list of `@requires` symbols. If the resource is not part of the217* dependency graph, both are null.218*/219private function getProvidesAndRequires($name, $data) {220$parser = new PhutilDocblockParser();221222$matches = array();223$ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);224if (!$ok) {225throw new Exception(226pht(227'Resource "%s" does not have a header doc comment. Encode '.228'dependency data in a header docblock.',229$name));230}231232list($description, $metadata) = $parser->parse($matches[0]);233234$provides = $this->parseResourceSymbolList(idx($metadata, 'provides'));235$requires = $this->parseResourceSymbolList(idx($metadata, 'requires'));236if (!$provides) {237// Tests and documentation-only JS is permitted to @provide no targets.238return array(null, null);239}240241if (count($provides) > 1) {242throw new Exception(243pht(244'Resource "%s" must %s at most one Celerity target.',245$name,246'@provide'));247}248249return array(head($provides), $requires);250}251252/**253* Check for dependency cycles in the resource graph. Raises an exception if254* a cycle is detected.255*256* @param map<string, list<string>> Map of `@provides` symbols to their257* `@requires` symbols.258* @return void259*/260private function detectGraphCycles(array $nodes) {261$graph = id(new CelerityResourceGraph())262->addNodes($nodes)263->setResourceGraph($nodes)264->loadGraph();265266foreach ($nodes as $provides => $requires) {267$cycle = $graph->detectCycles($provides);268if ($cycle) {269throw new Exception(270pht(271'Cycle detected in resource graph: %s',272implode(' > ', $cycle)));273}274}275}276277/**278* Build package specifications for a given resource source.279*280* @param CelerityPhysicalResources Resource source to rebuild.281* @param map<string, string> Map of `@provides` to hashes.282* @param map<string, string> Map of hashes to resource names.283* @return map<string, map<string, string>> Package information maps.284*/285private function rebuildPackages(286CelerityPhysicalResources $resources,287array $symbol_map,288array $reverse_map) {289290$package_map = array();291292$package_spec = $resources->getResourcePackages();293foreach ($package_spec as $package_name => $package_symbols) {294$type = null;295$hashes = array();296foreach ($package_symbols as $symbol) {297$symbol_hash = idx($symbol_map, $symbol);298if ($symbol_hash === null) {299throw new Exception(300pht(301'Package specification for "%s" includes "%s", but that symbol '.302'is not %s by any resource.',303$package_name,304$symbol,305'@provided'));306}307308$resource_name = $reverse_map[$symbol_hash];309$resource_type = $resources->getResourceType($resource_name);310if ($type === null) {311$type = $resource_type;312} else if ($type !== $resource_type) {313throw new Exception(314pht(315'Package specification for "%s" includes resources of multiple '.316'types (%s, %s). Each package may only contain one type of '.317'resource.',318$package_name,319$type,320$resource_type));321}322323$hashes[] = $symbol.':'.$symbol_hash;324}325326$hash = $this->newResourceHash(implode("\n", $hashes));327$package_map[$package_name] = array(328'hash' => $hash,329'symbols' => $package_symbols,330);331}332333return $package_map;334}335336private function mergeNameMaps(array $maps) {337$result = array();338$origin = array();339340foreach ($maps as $map) {341list($map_name, $data) = $map;342foreach ($data as $name => $hash) {343if (empty($result[$name])) {344$result[$name] = $hash;345$origin[$name] = $map_name;346} else {347$old = $origin[$name];348$new = $map_name;349throw new Exception(350pht(351'Resource source defines two resources with the same name, '.352'"%s". One is defined in the "%s" map; the other in the "%s" '.353'map. Each resource must have a unique name.',354$name,355$old,356$new));357}358}359}360return $result;361}362363private function parseResourceSymbolList($list) {364if (!$list) {365return array();366}367368// This is valid:369//370// @requires x y371//372// But so is this:373//374// @requires x375// @requires y376//377// Accept either form and produce a list of symbols.378379$list = (array)$list;380381// We can get `true` values if there was a bare `@requires` in the input.382foreach ($list as $key => $item) {383if ($item === true) {384unset($list[$key]);385}386}387388$list = implode(' ', $list);389$list = trim($list);390$list = preg_split('/\s+/', $list);391$list = array_filter($list);392393return $list;394}395396private function newResourceHash($data) {397// This HMAC key is a static, hard-coded value because we don't want the398// hashes in the map to depend on database state: when two different399// developers regenerate the map, they should end up with the same output.400401$hash = PhabricatorHash::digestHMACSHA256($data, 'celerity-resource-data');402403return substr($hash, 0, 8);404}405406}407408409