setName('install')
->addOption(
'symlink',
's',
InputOption::VALUE_NONE,
'Symlink the required bits'
)
->addOption(
'plugin',
'p',
InputOption::VALUE_REQUIRED,
'Install plugin (symlink)'
)
->addOption(
'theme',
't',
InputOption::VALUE_REQUIRED,
'Install theme (symlink)'
)
->addArgument(
'destination',
InputArgument::OPTIONAL,
'Where to install the required bits (default to current project)'
)
->setDescription('Installs the dependencies needed by Grav. Optionally can create symbolic links')
->setHelp('The install command installs the dependencies needed by Grav. Optionally can create symbolic links');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$dependencies_file = '.dependencies';
$this->destination = $input->getArgument('destination') ?: GRAV_WEBROOT;
// fix trailing slash
$this->destination = rtrim($this->destination, DS) . DS;
$this->user_path = $this->destination . GRAV_USER_PATH . DS;
if ($local_config_file = $this->loadLocalConfig()) {
$io->writeln('Read local config from ' . $local_config_file . '');
}
// Look for dependencies file in ROOT and USER dir
if (file_exists($this->user_path . $dependencies_file)) {
$file = YamlFile::instance($this->user_path . $dependencies_file);
} elseif (file_exists($this->destination . $dependencies_file)) {
$file = YamlFile::instance($this->destination . $dependencies_file);
} else {
$io->writeln('ERROR Missing .dependencies file in user/ folder');
if ($input->getArgument('destination')) {
$io->writeln('HINT Are you trying to install a plugin or a theme? Make sure you use bin/gpm install , not bin/grav install. This command is only used to install Grav skeletons.');
} else {
$io->writeln('HINT 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.');
}
return 1;
}
$this->config = $file->content();
$file->free();
// If no config, fail.
if (!$this->config) {
$io->writeln('ERROR invalid YAML in ' . $dependencies_file);
return 1;
}
$plugin = $input->getOption('plugin');
$theme = $input->getOption('theme');
$name = $plugin ?? $theme;
$symlink = $name || $input->getOption('symlink');
if (!$symlink) {
// Updates composer first
$io->writeln("\nInstalling vendor dependencies");
$io->writeln($this->composerUpdate(GRAV_ROOT, 'install'));
$error = $this->gitclone();
} else {
$type = $name ? ($plugin ? 'plugin' : 'theme') : null;
$error = $this->symlink($name, $type);
}
return $error;
}
/**
* Clones from Git
*
* @return int
*/
private function gitclone(): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('Cloning Bits');
$io->writeln('============');
$io->newLine();
$error = 0;
$this->destination = rtrim($this->destination, DS);
foreach ($this->config['git'] as $repo => $data) {
$path = $this->destination . DS . $data['path'];
if (!file_exists($path)) {
exec('cd ' . escapeshellarg($this->destination) . ' && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return);
if (!$return) {
$io->writeln('SUCCESS cloned ' . $data['url'] . ' -> ' . $path . '');
} else {
$io->writeln('ERROR cloning ' . $data['url']);
$error = 1;
}
$io->newLine();
} else {
$io->writeln('' . $path . ' already exists, skipping...');
$io->newLine();
}
}
return $error;
}
/**
* Symlinks
*
* @param string|null $name
* @param string|null $type
* @return int
*/
private function symlink(string $name = null, string $type = null): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('Symlinking Bits');
$io->writeln('===============');
$io->newLine();
if (!$this->local_config) {
$io->writeln('No local configuration available, aborting...');
$io->newLine();
return 1;
}
$error = 0;
$this->destination = rtrim($this->destination, DS);
if ($name) {
$src = "grav-{$type}-{$name}";
$links = [
$name => [
'scm' => 'github', // TODO: make configurable
'src' => $src,
'path' => "user/{$type}s/{$name}"
]
];
} else {
$links = $this->config['links'];
}
foreach ($links as $name => $data) {
$scm = $data['scm'] ?? null;
$src = $data['src'] ?? null;
$path = $data['path'] ?? null;
if (!isset($scm, $src, $path)) {
$io->writeln("Dependency '$name' has broken configuration, skipping...");
$io->newLine();
$error = 1;
continue;
}
$locations = (array) $this->local_config["{$scm}_repos"];
$to = $this->destination . DS . $path;
$from = null;
foreach ($locations as $location) {
$test = rtrim($location, '\\/') . DS . $src;
if (file_exists($test)) {
$from = $test;
continue;
}
}
if (is_link($to) && !realpath($to)) {
$io->writeln('Removed broken symlink '. $path .'');
unlink($to);
}
if (null === $from) {
$io->writeln('source for ' . $src . ' does not exists, skipping...');
$io->newLine();
$error = 1;
} elseif (!file_exists($to)) {
$error = $this->addSymlinks($from, $to, ['name' => $name, 'src' => $src, 'path' => $path]);
$io->newLine();
} else {
$io->writeln('destination: ' . $path . ' already exists, skipping...');
$io->newLine();
}
}
return $error;
}
private function addSymlinks(string $from, string $to, array $options): int
{
$io = $this->getIO();
$hebe = $this->readHebe($from);
if (null === $hebe) {
symlink($from, $to);
$io->writeln('SUCCESS symlinked ' . $options['src'] . ' -> ' . $options['path'] . '');
} else {
$to = GRAV_ROOT;
$name = $options['name'];
$io->writeln("Processing {$name}");
foreach ($hebe as $section => $symlinks) {
foreach ($symlinks as $symlink) {
$src = trim($symlink['source'], '/');
$dst = trim($symlink['destination'], '/');
$s = "{$from}/{$src}";
$d = "{$to}/{$dst}";
if (is_link($d) && !realpath($d)) {
unlink($d);
$io->writeln(' Removed broken symlink '. $dst .'');
}
if (!file_exists($d)) {
symlink($s, $d);
$io->writeln(' symlinked ' . $src . ' -> ' . $dst . '');
}
}
}
$io->writeln('SUCCESS');
}
return 0;
}
private function readHebe(string $folder): ?array
{
$filename = "{$folder}/hebe.json";
if (!is_file($filename)) {
return null;
}
$formatter = new JsonFormatter();
$file = new JsonFile($filename, $formatter);
$hebe = $file->load();
$paths = $hebe['platforms']['grav']['nodes'] ?? null;
return is_array($paths) ? $paths : null;
}
}