Skip to content

Make assets:install smarter with symlinks #11312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
40 changes: 30 additions & 10 deletions src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Finder\Finder;

/**
Expand Down Expand Up @@ -47,7 +48,7 @@ protected function configure()
"Resources/public" directory of each bundle will be copied into it.

To create a symlink to each bundle instead of copying its assets, use the
<info>--symlink</info> option:
<info>--symlink</info> option (will fall back to hard copies when symbolic links aren't possible:

<info>php %command.full_name% web --symlink</info>

Expand All @@ -73,17 +74,17 @@ protected function execute(InputInterface $input, OutputInterface $output)
throw new \InvalidArgumentException(sprintf('The target directory "%s" does not exist.', $input->getArgument('target')));
}

if (!function_exists('symlink') && $input->getOption('symlink')) {
throw new \InvalidArgumentException('The symlink() function is not available on your system. You need to install the assets without the --symlink option.');
}

$filesystem = $this->getContainer()->get('filesystem');

// Create the bundles directory otherwise symlink will fail.
$bundlesDir = $targetArg.'/bundles/';
$filesystem->mkdir($bundlesDir, 0777);

$output->writeln(sprintf('Installing assets as <comment>%s</comment>', $input->getOption('symlink') ? 'symlinks' : 'hard copies'));
if ($input->getOption('symlink')) {
$output->writeln('Trying to install assets as symbolic links.');
} else {
$output->writeln('Installing assets as <comment>hard copies</comment>');
}

foreach ($this->getContainer()->get('kernel')->getBundles() as $bundle) {
if (is_dir($originDir = $bundle->getPath().'/Resources/public')) {
Expand All @@ -99,13 +100,32 @@ protected function execute(InputInterface $input, OutputInterface $output)
} else {
$relativeOriginDir = $originDir;
}
$filesystem->symlink($relativeOriginDir, $targetDir);

try {
$filesystem->symlink($relativeOriginDir, $targetDir);
$output->writeln('The assets were installed using symbolic links.');
} catch (IOException $e) {
$this->hardCopy($originDir, $targetDir);
$output->writeln('It looks like your system doesn\'t support symbolic links, so the assets were installed by copying them.');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks perfect, but I don't think that it'll catch the case where you're on a VM with a Windows host machine, you're sharing the Symfony directory with the host machine, and you don't have proper permissions to actually create symlinks on the Windows machine (see: #11297 (comment)).

The reason I don't think that this will work for that one case is that I don't think these users are currently seeing an IOException - the assets:install just thinks that symlinks work (I could be wrong here, but pretty sure I'm not).

So, I think we need to actually check for the integrity of the symlink - e.g. can you look inside of its contents or not. This could actually go into the Filesystem::symlink method, if we could find a clean way, consistent way of doing this check.

} else {
$filesystem->mkdir($targetDir, 0777);
// We use a custom iterator to ignore VCS files
$filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
$this->hardCopy($originDir, $targetDir);
}
}
}
}

/**
* @param string $originDir
* @param string $targetDir
*/
private function hardCopy($originDir, $targetDir)
{
$filesystem = $this->getContainer()->get('filesystem');

$filesystem->mkdir($targetDir, 0777);
// We use a custom iterator to ignore VCS files
$filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
}

}
12 changes: 8 additions & 4 deletions src/Symfony/Component/Filesystem/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,10 @@ public function rename($origin, $target, $overwrite = false)
*/
public function symlink($originDir, $targetDir, $copyOnWindows = false)
{
if (!function_exists('symlink') && $copyOnWindows) {
$this->mirror($originDir, $targetDir);
$onWindows = strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN';

if ($onWindows && $copyOnWindows) {
$this->mirror($originDir, $targetDir);
return;
}

Expand All @@ -293,9 +294,12 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false)
throw new IOException('Unable to create symlink due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?');
}
}

throw new IOException(sprintf('Failed to create symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir);
}

if (!file_exists($targetDir)) {
throw new IOException(sprintf('Symbolic link "%s" is created but appears to be broken.', $targetDir), 0, null, $targetDir);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need someone on a VM that has a Windows host machine (and where the project files are shared with the host machine) to try this and see if this properly catches the failure: #11297 (comment)

}
}

Expand Down Expand Up @@ -374,7 +378,7 @@ public function mirror($originDir, $targetDir, \Traversable $iterator = null, $o
}

$copyOnWindows = false;
if (isset($options['copy_on_windows']) && !function_exists('symlink')) {
if (isset($options['copy_on_windows'])) {
$copyOnWindows = $options['copy_on_windows'];
}

Expand Down