InstallCommand.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. <?php
  2. /**
  3. * @package Grav\Console\Cli
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Console\Cli;
  9. use Grav\Console\GravCommand;
  10. use Grav\Framework\File\Formatter\JsonFormatter;
  11. use Grav\Framework\File\JsonFile;
  12. use RocketTheme\Toolbox\File\YamlFile;
  13. use Symfony\Component\Console\Input\InputArgument;
  14. use Symfony\Component\Console\Input\InputOption;
  15. use function is_array;
  16. /**
  17. * Class InstallCommand
  18. * @package Grav\Console\Cli
  19. */
  20. class InstallCommand extends GravCommand
  21. {
  22. /** @var array */
  23. protected $config;
  24. /** @var string */
  25. protected $destination;
  26. /** @var string */
  27. protected $user_path;
  28. /**
  29. * @return void
  30. */
  31. protected function configure(): void
  32. {
  33. $this
  34. ->setName('install')
  35. ->addOption(
  36. 'symlink',
  37. 's',
  38. InputOption::VALUE_NONE,
  39. 'Symlink the required bits'
  40. )
  41. ->addOption(
  42. 'plugin',
  43. 'p',
  44. InputOption::VALUE_REQUIRED,
  45. 'Install plugin (symlink)'
  46. )
  47. ->addOption(
  48. 'theme',
  49. 't',
  50. InputOption::VALUE_REQUIRED,
  51. 'Install theme (symlink)'
  52. )
  53. ->addArgument(
  54. 'destination',
  55. InputArgument::OPTIONAL,
  56. 'Where to install the required bits (default to current project)'
  57. )
  58. ->setDescription('Installs the dependencies needed by Grav. Optionally can create symbolic links')
  59. ->setHelp('The <info>install</info> command installs the dependencies needed by Grav. Optionally can create symbolic links');
  60. }
  61. /**
  62. * @return int
  63. */
  64. protected function serve(): int
  65. {
  66. $input = $this->getInput();
  67. $io = $this->getIO();
  68. $dependencies_file = '.dependencies';
  69. $this->destination = $input->getArgument('destination') ?: GRAV_WEBROOT;
  70. // fix trailing slash
  71. $this->destination = rtrim($this->destination, DS) . DS;
  72. $this->user_path = $this->destination . GRAV_USER_PATH . DS;
  73. if ($local_config_file = $this->loadLocalConfig()) {
  74. $io->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
  75. }
  76. // Look for dependencies file in ROOT and USER dir
  77. if (file_exists($this->user_path . $dependencies_file)) {
  78. $file = YamlFile::instance($this->user_path . $dependencies_file);
  79. } elseif (file_exists($this->destination . $dependencies_file)) {
  80. $file = YamlFile::instance($this->destination . $dependencies_file);
  81. } else {
  82. $io->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
  83. if ($input->getArgument('destination')) {
  84. $io->writeln('<yellow>HINT</yellow> <info>Are you trying to install a plugin or a theme? Make sure you use <cyan>bin/gpm install <something></cyan>, not <cyan>bin/grav install</cyan>. This command is only used to install Grav skeletons.');
  85. } else {
  86. $io->writeln('<yellow>HINT</yellow> <info>Are you trying to install Grav? Grav is already installed. You need to run this command only if you download a skeleton from GitHub directly.');
  87. }
  88. return 1;
  89. }
  90. $this->config = $file->content();
  91. $file->free();
  92. // If no config, fail.
  93. if (!$this->config) {
  94. $io->writeln('<red>ERROR</red> invalid YAML in ' . $dependencies_file);
  95. return 1;
  96. }
  97. $plugin = $input->getOption('plugin');
  98. $theme = $input->getOption('theme');
  99. $name = $plugin ?? $theme;
  100. $symlink = $name || $input->getOption('symlink');
  101. if (!$symlink) {
  102. // Updates composer first
  103. $io->writeln("\nInstalling vendor dependencies");
  104. $io->writeln($this->composerUpdate(GRAV_ROOT, 'install'));
  105. $error = $this->gitclone();
  106. } else {
  107. $type = $name ? ($plugin ? 'plugin' : 'theme') : null;
  108. $error = $this->symlink($name, $type);
  109. }
  110. return $error;
  111. }
  112. /**
  113. * Clones from Git
  114. *
  115. * @return int
  116. */
  117. private function gitclone(): int
  118. {
  119. $io = $this->getIO();
  120. $io->newLine();
  121. $io->writeln('<green>Cloning Bits</green>');
  122. $io->writeln('============');
  123. $io->newLine();
  124. $error = 0;
  125. $this->destination = rtrim($this->destination, DS);
  126. foreach ($this->config['git'] as $repo => $data) {
  127. $path = $this->destination . DS . $data['path'];
  128. if (!file_exists($path)) {
  129. exec('cd ' . escapeshellarg($this->destination) . ' && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return);
  130. if (!$return) {
  131. $io->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
  132. } else {
  133. $io->writeln('<red>ERROR</red> cloning <magenta>' . $data['url']);
  134. $error = 1;
  135. }
  136. $io->newLine();
  137. } else {
  138. $io->writeln('<yellow>' . $path . ' already exists, skipping...</yellow>');
  139. $io->newLine();
  140. }
  141. }
  142. return $error;
  143. }
  144. /**
  145. * Symlinks
  146. *
  147. * @param string|null $name
  148. * @param string|null $type
  149. * @return int
  150. */
  151. private function symlink(string $name = null, string $type = null): int
  152. {
  153. $io = $this->getIO();
  154. $io->newLine();
  155. $io->writeln('<green>Symlinking Bits</green>');
  156. $io->writeln('===============');
  157. $io->newLine();
  158. if (!$this->local_config) {
  159. $io->writeln('<red>No local configuration available, aborting...</red>');
  160. $io->newLine();
  161. return 1;
  162. }
  163. $error = 0;
  164. $this->destination = rtrim($this->destination, DS);
  165. if ($name) {
  166. $src = "grav-{$type}-{$name}";
  167. $links = [
  168. $name => [
  169. 'scm' => 'github', // TODO: make configurable
  170. 'src' => $src,
  171. 'path' => "user/{$type}s/{$name}"
  172. ]
  173. ];
  174. } else {
  175. $links = $this->config['links'];
  176. }
  177. foreach ($links as $name => $data) {
  178. $scm = $data['scm'] ?? null;
  179. $src = $data['src'] ?? null;
  180. $path = $data['path'] ?? null;
  181. if (!isset($scm, $src, $path)) {
  182. $io->writeln("<red>Dependency '$name' has broken configuration, skipping...</red>");
  183. $io->newLine();
  184. $error = 1;
  185. continue;
  186. }
  187. $locations = (array) $this->local_config["{$scm}_repos"];
  188. $to = $this->destination . DS . $path;
  189. $from = null;
  190. foreach ($locations as $location) {
  191. $test = rtrim($location, '\\/') . DS . $src;
  192. if (file_exists($test)) {
  193. $from = $test;
  194. continue;
  195. }
  196. }
  197. if (is_link($to) && !realpath($to)) {
  198. $io->writeln('<yellow>Removed broken symlink '. $path .'</yellow>');
  199. unlink($to);
  200. }
  201. if (null === $from) {
  202. $io->writeln('<red>source for ' . $src . ' does not exists, skipping...</red>');
  203. $io->newLine();
  204. $error = 1;
  205. } elseif (!file_exists($to)) {
  206. $error = $this->addSymlinks($from, $to, ['name' => $name, 'src' => $src, 'path' => $path]);
  207. $io->newLine();
  208. } else {
  209. $io->writeln('<yellow>destination: ' . $path . ' already exists, skipping...</yellow>');
  210. $io->newLine();
  211. }
  212. }
  213. return $error;
  214. }
  215. private function addSymlinks(string $from, string $to, array $options): int
  216. {
  217. $io = $this->getIO();
  218. $hebe = $this->readHebe($from);
  219. if (null === $hebe) {
  220. symlink($from, $to);
  221. $io->writeln('<green>SUCCESS</green> symlinked <magenta>' . $options['src'] . '</magenta> -> <cyan>' . $options['path'] . '</cyan>');
  222. } else {
  223. $to = GRAV_ROOT;
  224. $name = $options['name'];
  225. $io->writeln("Processing <magenta>{$name}</magenta>");
  226. foreach ($hebe as $section => $symlinks) {
  227. foreach ($symlinks as $symlink) {
  228. $src = trim($symlink['source'], '/');
  229. $dst = trim($symlink['destination'], '/');
  230. $s = "{$from}/{$src}";
  231. $d = "{$to}/{$dst}";
  232. if (is_link($d) && !realpath($d)) {
  233. unlink($d);
  234. $io->writeln(' <yellow>Removed broken symlink '. $dst .'</yellow>');
  235. }
  236. if (!file_exists($d)) {
  237. symlink($s, $d);
  238. $io->writeln(' symlinked <magenta>' . $src . '</magenta> -> <cyan>' . $dst . '</cyan>');
  239. }
  240. }
  241. }
  242. $io->writeln('<green>SUCCESS</green>');
  243. }
  244. return 0;
  245. }
  246. private function readHebe(string $folder): ?array
  247. {
  248. $filename = "{$folder}/hebe.json";
  249. if (!is_file($filename)) {
  250. return null;
  251. }
  252. $formatter = new JsonFormatter();
  253. $file = new JsonFile($filename, $formatter);
  254. $hebe = $file->load();
  255. $paths = $hebe['platforms']['grav']['nodes'] ?? null;
  256. return is_array($paths) ? $paths : null;
  257. }
  258. }