Force composer to symlink local package

Viewed 24388

I'm trying to find a proper way to force composer to symlink local package.

I know there is a question How to force Composer to download a local package? but it does not answers my questions. I need to make sure it works as expected because I'm going to use this in CI workflow.

Having project's composer.json like:

{
    "name": "Some project",
    "type": "project",
    "minimum-stability": "dev",
    "prefer-stable": true,
    "repositories": [{
        "type": "path", 
        "url": "packages/*/*"
    }]
}

And package's composer.json (packages/sample/package):

{
    "name": "Sample package",
    "version": "1.0.0"
}

Let's assume that:

  • sample/package:1.0.0 is published in packagist (commit aaaaaa) - with unmodified composer.json
  • sample/package locally is checked out on commit bbbbbb
  • I can't modify version of sample/package locally

Command 1:

$ composer require sample/package

Package is fetched from packagist (version 1.0.0, commit aaaaaa).

Command 2:

$ composer require sample/package:@dev

Package is symlinked from local version to vendor directory (version 1.0.0, commit bbbbbb, symlinked).

Questions are:

  1. Why Command 1 downloads package from packagist despite minimum-stability option? Version constraint @dev lets you enforce different stability but it already down to dev with project config.
  2. Will Command 2 create symlink to local package in every case?
  3. Is there a better way than Command 2 to make sure local package gets symlinked?
4 Answers

Why Command 1 downloads package from packagist despite minimum-stability option? Version constraint @dev lets you enforce different stability but it already down to dev with project config.

My best educated guess would be that when no version constraint is provided in composer require sample/package, Composer will still try to find a stable version to install, because of "prefer-stable": true. When you set this option to false (or provide @dev version constraint for the package explicitly) you should see Composer using the latest available version (dev-master on commit bbbbbb).

Will Command 2 create symlink to local package in every case?

Not in every case. You can force Composer to always symlink packages using "symlink": true option.

From https://getcomposer.org/doc/05-repositories.md#path:

The local package will be symlinked if possible, in which case the output in the console will read Symlinked from ../../packages/my-package. If symlinking is not possible the package will be copied. In that case, the console will output Mirrored from ../../packages/my-package.

Instead of default fallback strategy you can force to use symlink with "symlink": true or mirroring with "symlink": false option. Forcing mirroring can be useful when deploying or generating package from a monolithic repository.

Is there a better way than Command 2 to make sure local package gets symlinked?

You should probably avoid dropping "prefer-stable": true from composer.json as this would affect all dependencies.

I'd suggest to just make sure you explicitly require @dev version for sample/package and set "symlink": true for the local repository. This would result in something like:

{
    "name": "Some project",
    "type": "project",
    "minimum-stability": "dev",
    "prefer-stable": true,
    "repositories": [{
        "type": "path", 
        "url": "packages/*/*",
        "symlink": true
    }],
    "require": {
        "sample/package": "@dev"
    }

}

If the very useful information provided by @Maciek Lewkowicz doesn't work, maybe you are in this situation:

  1. You are using a specific branch (for example, dev-master);
  2. The branch you are using is from your remote repository;
  3. You are trying to symlink the same branch, but this time using the path (so, the only thing you changed in composer.json was the source of the package, setting the repo in the repositories key of the composer.json file).

In this situation a simple composer update will not work: Composer will not intercept the change: for Composer is irrelevant that you changed the repository from the remote to the local path one. It only checks that the branch is the same and so doesn't update at all.

So, in this case, nothing will work: it will not work to use symlink: force nor it will work to change the preferences about the source and so on.

The only thing that will work is to change the branch.

So, for example, your composer.json will be something like this:

{
    ...
    "require": {
        ...
-       "your/package": "dev-master",
+       "your/package": "dev-dev",
        ...
    },
    ...
    "repositories": [
        {
            "type": "path",
            "url": "/Path/To/Your/Local/package"
        }
    ]
}

Here dev-dev is a new branch called dev: changing the branch from dev-master to dev-dev will force Composer to download again the package as the required branch is not dev-master anymore.

In downloading again the package, Composer will also check that the new source is now a local path and so it will symlink the folder.

Also note that composer install will not work, also if you change the branch name.

YOU HAVE TO USE composer update.

Another reason this might not work, is when you have multiple entries in repository. Composer will use the repository definition it first finds the package in.

In our case, we use a custom Satis repository. When defining the local package after the satis repo, it would never symlink and always use the Satis version. When defining the local package first, it does work.

Working example:

    "repositories": {
        "our/local-package": {
            "type": "path",
            "url": "../../some-local-package",
            "options": {
                "symlink": true
            }
        },
        "satis": {
            "type": "composer",
            "url": "http://our.satis.url/"
        }
    }

And then call composer require our/local-package:@dev

To expand a little bit on the already great answers here, I ran into the same problem but the solution was slightly different:

When you have the same "version" available (branch, tag, hash) in your local package AND in a remote repo, in case composer can not resolve the dependencies of your local package, it will just silently drag the version from the remote repo, as long as those dependencies can be resolved.

In order to debug this you can add some weirdo version (e.g. 100) to the "version" attribute of your local packages package.json and then require it with that version, thus showing you which dependency can not be fulfilled (and you change that then).

Related