|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Consolidation\Cgr; |
|
4
|
|
|
|
|
5
|
|
|
class Application |
|
6
|
|
|
{ |
|
7
|
|
|
/** |
|
8
|
|
|
* Run the cgr tool, a safer alternative to `composer global require`. |
|
9
|
|
|
* |
|
10
|
|
|
* @param array $argv The global $argv array passed in by PHP |
|
11
|
|
|
* @param string $home The path to the user's home directory |
|
12
|
|
|
* @return integer |
|
13
|
|
|
*/ |
|
14
|
|
|
public function run($argv, $home) |
|
15
|
|
|
{ |
|
16
|
|
|
$commandList = $this->parseArgvAndGetCommandList($argv, $home); |
|
17
|
|
|
return $this->runCommandList($commandList); |
|
18
|
|
|
} |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* Figure out everything we're going to do, but don't do any of it |
|
22
|
|
|
* yet, just return the command objects to run. |
|
23
|
|
|
*/ |
|
24
|
|
|
public function parseArgvAndGetCommandList($argv, $home) |
|
25
|
|
|
{ |
|
26
|
|
|
$optionDefaultValues = $this->getDefaultOptionValues($home); |
|
27
|
|
|
list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues); |
|
28
|
|
|
list($projects, $composerArgs) = $this->separateProjectsFromArgs($argv, $options); |
|
|
|
|
|
|
29
|
|
|
|
|
30
|
|
|
$commandList = $this->getCommandStringList($composerArgs, $projects, $options); |
|
31
|
|
|
return $commandList; |
|
32
|
|
|
} |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* Run all of the commands in a list. Abort early if any fail. |
|
36
|
|
|
* |
|
37
|
|
|
* @param array $commandList An array of CommandToExec |
|
38
|
|
|
* @return integer |
|
39
|
|
|
*/ |
|
40
|
|
|
public function runCommandList($commandList) |
|
41
|
|
|
{ |
|
42
|
|
|
foreach ($commandList as $command) { |
|
43
|
|
|
$exitCode = $command->run(); |
|
44
|
|
|
if ($exitCode) { |
|
45
|
|
|
return $exitCode; |
|
46
|
|
|
} |
|
47
|
|
|
} |
|
48
|
|
|
return 0; |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* Return an array containing a list of commands to execute. Depending on |
|
53
|
|
|
* the composition of the aguments and projects parameters, this list will |
|
54
|
|
|
* contain either a single command string to call through to composer (if |
|
55
|
|
|
* cgr is being used as a composer alias), or it will contain a list of |
|
56
|
|
|
* appropriate replacement 'composer global require' commands that install |
|
57
|
|
|
* each project in its own installation directory, while installing each |
|
58
|
|
|
* projects' binaries in the global Composer bin directory, |
|
59
|
|
|
* ~/.composer/vendor/bin. |
|
60
|
|
|
* |
|
61
|
|
|
* @param array $composerArgs |
|
62
|
|
|
* @param array $projects |
|
63
|
|
|
* @param array $options |
|
64
|
|
|
* @return CommandToExec |
|
65
|
|
|
*/ |
|
66
|
|
|
public function getCommandStringList($composerArgs, $projects, $options) |
|
67
|
|
|
{ |
|
68
|
|
|
$command = $options['composer-path']; |
|
69
|
|
|
if (empty($projects)) { |
|
70
|
|
|
return array(new CommandToExec($command, $composerArgs)); |
|
71
|
|
|
} |
|
72
|
|
|
return $this->globalRequire($command, $composerArgs, $projects, $options); |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* Return our list of default option values, with paths relative to |
|
77
|
|
|
* the provided home directory. |
|
78
|
|
|
* @param string $home The user's home directory |
|
79
|
|
|
* @return array |
|
80
|
|
|
*/ |
|
81
|
|
|
public function getDefaultOptionValues($home) |
|
82
|
|
|
{ |
|
83
|
|
|
return array( |
|
84
|
|
|
'composer-path' => 'composer', |
|
85
|
|
|
'base-dir' => "$home/.composer/global", |
|
86
|
|
|
'bin-dir' => "$home/.composer/vendor/bin", |
|
87
|
|
|
); |
|
88
|
|
|
} |
|
89
|
|
|
|
|
90
|
|
|
/** |
|
91
|
|
|
* We use our own special-purpose argv parser. The options that apply |
|
92
|
|
|
* to this tool are identified by a simple associative array, where |
|
93
|
|
|
* the key is the option name, and the value is its default value. |
|
94
|
|
|
* The result of this function is an array of two items containing: |
|
95
|
|
|
* - An array of the items in $argv not used to set an option value |
|
96
|
|
|
* - An array of options containing the user-specified or default values |
|
97
|
|
|
* |
|
98
|
|
|
* @param array $argv The global $argv passed in by php |
|
99
|
|
|
* @param array $optionDefaultValues An associative array |
|
100
|
|
|
* @return array |
|
101
|
|
|
*/ |
|
102
|
|
|
public function parseOutOurOptions($argv, $optionDefaultValues) |
|
103
|
|
|
{ |
|
104
|
|
|
array_shift($argv); |
|
105
|
|
|
$passAlongArgvItems = array(); |
|
106
|
|
|
$options = array(); |
|
107
|
|
|
while (!empty($argv)) { |
|
108
|
|
|
$arg = array_shift($argv); |
|
109
|
|
|
if ((substr($arg, 0, 2) == '--') && array_key_exists(substr($arg, 2), $optionDefaultValues)) { |
|
110
|
|
|
$options[substr($arg, 2)] = array_shift($argv); |
|
111
|
|
|
} else { |
|
112
|
|
|
$passAlongArgvItems[] = $arg; |
|
113
|
|
|
} |
|
114
|
|
|
} |
|
115
|
|
|
return array($passAlongArgvItems, $options + $optionDefaultValues); |
|
116
|
|
|
} |
|
117
|
|
|
|
|
118
|
|
|
/** |
|
119
|
|
|
* After our options are removed by parseOutOurOptions, those items remaining |
|
120
|
|
|
* in $argv will be separated into a list of projects and versions, and |
|
121
|
|
|
* anything else that is not a project:version. Returns an array of two |
|
122
|
|
|
* items containing: |
|
123
|
|
|
* - An associative array, where the key is the project name and the value |
|
124
|
|
|
* is the version (or an empty string, if no version was specified) |
|
125
|
|
|
* - The remaining $argv items not used to build the projects array. |
|
126
|
|
|
* |
|
127
|
|
|
* @param array $argv The $argv array from parseOutOurOptions() |
|
128
|
|
|
* @return array |
|
129
|
|
|
*/ |
|
130
|
|
|
public function separateProjectsFromArgs($argv) |
|
131
|
|
|
{ |
|
132
|
|
|
$composerArgs = array(); |
|
133
|
|
|
$projects = array(); |
|
134
|
|
|
$sawGlobal = false; |
|
135
|
|
|
foreach ($argv as $arg) { |
|
136
|
|
|
if ($arg[0] == '-') { |
|
137
|
|
|
// Any flags (first character is '-') will just be passed |
|
138
|
|
|
// through to to composer. Flags interpreted by cgr have |
|
139
|
|
|
// already been removed from $argv. |
|
140
|
|
|
$composerArgs[] = $arg; |
|
141
|
|
|
} elseif (strpos($arg, '/') !== false) { |
|
142
|
|
|
// Arguments containing a '/' name projects. We will split |
|
143
|
|
|
// the project from its version, allowing the separator |
|
144
|
|
|
// character to be either a '=' or a ':', and then store the |
|
145
|
|
|
// result in the $projects array. |
|
146
|
|
|
$projectAndVersion = explode(':', strtr($arg, '=', ':'), 2) + array('', ''); |
|
147
|
|
|
list($project, $version) = $projectAndVersion; |
|
148
|
|
|
$projects[$project] = $version; |
|
149
|
|
|
} elseif ($this->isComposerVersion($arg)) { |
|
150
|
|
|
// If an argument is a composer version, then we will alter |
|
151
|
|
|
// the last project we saw, attaching this version to it. |
|
152
|
|
|
// This allows us to handle 'a/b:1.0' and 'a/b 1.0' equivalently. |
|
153
|
|
|
$keys = array_keys($projects); |
|
154
|
|
|
$lastProject = array_pop($keys); |
|
155
|
|
|
unset($projects[$lastProject]); |
|
156
|
|
|
$projects[$lastProject] = $arg; |
|
157
|
|
|
} elseif ($arg == 'global') { |
|
158
|
|
|
// Make note if we see the 'global' command. |
|
159
|
|
|
$sawGlobal = true; |
|
160
|
|
|
} else { |
|
161
|
|
|
// If we see any command other than 'global require', |
|
162
|
|
|
// then we will pass *all* of the arguments through to |
|
163
|
|
|
// composer unchanged. We return an empty projects array |
|
164
|
|
|
// to indicate that this should be a pass-through call |
|
165
|
|
|
// to composer, rather than one or more calls to |
|
166
|
|
|
// 'composer require' to install global projects. |
|
167
|
|
|
if (($sawGlobal == false) || ($arg != 'require')) { |
|
|
|
|
|
|
168
|
|
|
return array(array(), $argv); |
|
169
|
|
|
} |
|
170
|
|
|
} |
|
171
|
|
|
} |
|
172
|
|
|
return array($projects, $composerArgs); |
|
173
|
|
|
} |
|
174
|
|
|
|
|
175
|
|
|
/** |
|
176
|
|
|
* Provide a safer version of `composer global require`. Each project |
|
177
|
|
|
* listed in $projects will be installed into its own project directory. |
|
178
|
|
|
* The binaries from each project will still be placed in the global |
|
179
|
|
|
* composer bin directory. |
|
180
|
|
|
* |
|
181
|
|
|
* @param string $command The path to composer |
|
182
|
|
|
* @param array $composerArgs Anything from the global $argv to be passed |
|
183
|
|
|
* on to Composer |
|
184
|
|
|
* @param array $projects A list of projects to install, with the key |
|
185
|
|
|
* specifying the project name, and the value specifying its version. |
|
186
|
|
|
* @param array $options User options from the command line; see |
|
187
|
|
|
* $optionDefaultValues in the main() function. |
|
188
|
|
|
* @return array |
|
189
|
|
|
*/ |
|
190
|
|
|
public function globalRequire($command, $composerArgs, $projects, $options) |
|
191
|
|
|
{ |
|
192
|
|
|
$globalBaseDir = $options['base-dir']; |
|
193
|
|
|
$binDir = $options['bin-dir']; |
|
194
|
|
|
$env = array("COMPOSER_BIN_DIR" => $binDir); |
|
195
|
|
|
$result = array(); |
|
196
|
|
|
foreach ($projects as $project => $version) { |
|
197
|
|
|
$installLocation = "$globalBaseDir/$project"; |
|
198
|
|
|
$projectWithVersion = $this->projectWithVersion($project, $version); |
|
199
|
|
|
$commandToExec = $this->globalRequireOne($command, $composerArgs, $projectWithVersion, $env, $installLocation); |
|
200
|
|
|
$result[] = $commandToExec; |
|
201
|
|
|
} |
|
202
|
|
|
return $result; |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
/** |
|
206
|
|
|
* Return $project:$version, or just $project if there is no $version. |
|
207
|
|
|
* |
|
208
|
|
|
* @param string $project The project to install |
|
209
|
|
|
* @param string $version The version desired |
|
210
|
|
|
* @return string |
|
211
|
|
|
*/ |
|
212
|
|
|
public function projectWithVersion($project, $version) |
|
213
|
|
|
{ |
|
214
|
|
|
if (empty($version)) { |
|
215
|
|
|
return $project; |
|
216
|
|
|
} |
|
217
|
|
|
return "$project:$version"; |
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
/** |
|
221
|
|
|
* Generate command string to call `composer require` to install one project. |
|
222
|
|
|
* |
|
223
|
|
|
* @param string $command The path to composer |
|
224
|
|
|
* @param array $composerArgs The arguments to pass to composer |
|
225
|
|
|
* @param string $projectWithVersion The project:version to install |
|
226
|
|
|
* @param array $env Environment to set prior to exec |
|
227
|
|
|
* @param string $installLocation Location to install the project |
|
228
|
|
|
* @return CommandToExec |
|
229
|
|
|
*/ |
|
230
|
|
|
public function globalRequireOne($command, $composerArgs, $projectWithVersion, $env, $installLocation) |
|
231
|
|
|
{ |
|
232
|
|
|
$projectSpecificArgs = array("--working-dir=$installLocation", 'require', $projectWithVersion); |
|
233
|
|
|
$arguments = array_merge($composerArgs, $projectSpecificArgs); |
|
234
|
|
|
return new CommandToExec($command, $arguments, $env, $installLocation); |
|
|
|
|
|
|
235
|
|
|
} |
|
236
|
|
|
|
|
237
|
|
|
/** |
|
238
|
|
|
* Identify an argument that could be a Composer version string. |
|
239
|
|
|
* |
|
240
|
|
|
* @param string $arg The argument to test |
|
241
|
|
|
* @return boolean |
|
242
|
|
|
*/ |
|
243
|
|
|
public function isComposerVersion($arg) |
|
244
|
|
|
{ |
|
245
|
|
|
$specialVersionChars = array('^', '~', '<', '>'); |
|
246
|
|
|
return is_numeric($arg[0]) || in_array($arg[0], $specialVersionChars); |
|
247
|
|
|
} |
|
248
|
|
|
} |
|
249
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignorePhpDoc annotation to the duplicate definition and it will be ignored.