package = new Package($this->loadJson($path), dirname(realpath($path))); $this->logger = new Logger(); } /** * set output function to use to output log messages * * @param callable|boolean $output callable that receives a single $line argument or boolean echo */ public function setOutput($output) { $this->logger->setOutput($output); } /** * Get path to target phar file (absolute path or relative to current directory) * * @return string */ public function getTarget() { if ($this->target === null) { $this->target = $this->package->getShortName() . '.phar'; } return $this->target; } /** * Set path to target phar file (absolute path or relative to current directory) * * If the given path is a directory, the default target (package short name) * will be appended automatically. * * @param string $target * @return $this */ public function setTarget($target) { // path is actually a directory => append package name if (is_dir($target)) { $this->target = null; $target = rtrim($target, '/') . '/' . $this->getTarget(); } $this->target = $target; return $this; } /** * Get path to main bin (relative to package directory) * * @return string * @throws \UnexpectedValueException */ public function getMain() { if ($this->main === null) { foreach ($this->package->getBins() as $path) { if (!file_exists($this->package->getDirectory() . $path)) { throw new \UnexpectedValueException('Bin file "' . $path . '" does not exist'); } $this->main = $path; break; } } return $this->main; } /** * set path to main bin (relative to package directory) * * @param string $main * @return $this */ public function setMain($main) { $this->main = $main; return $this; } /** * * @return Package */ public function getPackageRoot() { return $this->package; } /** * * @return Package[] */ public function getPackagesDependencies() { $packages = array(); $pathVendor = $this->package->getDirectory() . $this->package->getPathVendor(); // load all installed packages (use installed.json which also includes version instead of composer.lock) if (is_file($pathVendor . 'composer/installed.json')) { // file does not exist if there's nothing to be installed $installed = $this->loadJson($pathVendor . 'composer/installed.json'); // Composer 2.0 format wrapped in additional root key if (isset($installed['packages'])) { $installed = $installed['packages']; } foreach ($installed as $package) { $dir = $package['name'] . '/'; if (isset($package['target-dir'])) { $dir .= trim($package['target-dir'], '/') . '/'; } $dir = $pathVendor . $dir; $packages []= new Package($package, $dir); } } return $packages; } public function build() { $this->log('[' . $this->step . '/' . $this->step.'] Creating phar ' . $this->getTarget() . ''); $time = microtime(true); $pathVendor = $this->package->getDirectory() . $this->package->getPathVendor(); if (!is_dir($pathVendor)) { throw new \RuntimeException('Directory "' . $pathVendor . '" not properly installed, did you run "composer install"?'); } // get target and tempory file name to write to $target = $this->getTarget(); do { $tmp = $target . '.' . mt_rand() . '.phar'; } while (file_exists($tmp)); $targetPhar = new TargetPhar(new \Phar($tmp), $this); $this->log(' - Adding main package "' . $this->package->getName() . '"'); $targetPhar->addBundle($this->package->bundle()); $this->log(' - Adding composer base files'); // explicitly add composer autoloader $targetPhar->addFile($pathVendor . 'autoload.php'); // only add composer base directory (no sub-directories!) $targetPhar->buildFromIterator(new \GlobIterator($pathVendor . 'composer/*.*', \FilesystemIterator::KEY_AS_FILENAME)); foreach ($this->getPackagesDependencies() as $package) { $this->log(' - Adding dependency "' . $package->getName() . '" from "' . $this->getPathLocalToBase($package->getDirectory()) . '"'); $targetPhar->addBundle($package->bundle()); } $this->log(' - Setting main/stub'); $chmod = 0755; $main = $this->getMain(); if ($main === null) { $this->log(' WARNING: No main bin file defined! Resulting phar will NOT be executable'); } else { $generator = StubGenerator::create() ->index($main) ->extract(true) ->banner("Bundled by phar-composer with the help of php-box.\n\n@link https://github.com/clue/phar-composer"); $lines = file($this->package->getDirectory() . $main, FILE_IGNORE_NEW_LINES); if (substr($lines[0], 0, 2) === '#!') { $this->log(' Using referenced shebang "'. $lines[0] . '"'); $generator->shebang($lines[0]); // remove shebang from main file and add (overwrite) unset($lines[0]); $targetPhar->addFromString($main, implode("\n", $lines)); } $targetPhar->setStub($generator->generate()); $chmod = octdec(substr(decoct(fileperms($this->package->getDirectory() . $main)),-4)); $this->log(' Using referenced chmod ' . sprintf('%04o', $chmod)); } // stop buffering contents in memory and write to file // failure to write will emit a warning (ignore) and throw an (uncaught) exception try { @$targetPhar->stopBuffering(); $targetPhar = null; } catch (\PharException $e) { throw new \RuntimeException('Unable to write phar: ' . $e->getMessage()); } if ($chmod !== null) { $this->log(' Applying chmod ' . sprintf('%04o', $chmod)); if (chmod($tmp, $chmod) === false) { throw new \UnexpectedValueException('Unable to chmod target file "' . $target .'"'); } } if (file_exists($target)) { $this->log(' - Overwriting existing file ' . $target . ' (' . $this->getSize($target) . ')'); } if (@rename($tmp, $target) === false) { // retry renaming after sleeping to give slow network drives some time to flush data sleep(5); if (rename($tmp, $target) === false) { throw new \UnexpectedValueException('Unable to rename temporary phar archive to "'.$target.'"'); } } $time = max(microtime(true) - $time, 0); $this->log(''); $this->log(' OK - Creating ' . $this->getTarget() .' (' . $this->getSize($this->getTarget()) . ') completed after ' . round($time, 1) . 's'); } private function getSize($path) { return round(filesize($path) / 1024, 1) . ' KiB'; } public function getPathLocalToBase($path) { $root = $this->package->getDirectory(); if (strpos($path, $root) !== 0) { throw new \UnexpectedValueException('Path "' . $path . '" is not within base project path "' . $root . '"'); } return substr($path, strlen($root)); } public function log($message) { $this->logger->log($message); } public function setStep($step) { $this->step = $step; } /** * @param string $path * @return mixed * @throws \InvalidArgumentException */ private function loadJson($path) { $ret = @json_decode(file_get_contents($path), true); if ($ret === null) { throw new \InvalidArgumentException('Unable to parse given path "' . $path . '"', json_last_error()); } return $ret; } }