Issues (4868)

install-cli.php (4 issues)

1
#!/usr/bin/env php
2
<?php
3
/**
4
 * Install / update EGroupware - Command line interface
5
 *
6
 * Usage:
7
 *
8
 * - install-cli.php [-v|--verbose] [--use-prerelease] [<composer-args>] [(master|bugfix|release|<branch>|<tag>)]
9
 *   you can use composer install arguments like: --ignore-platform-reqs --no-dev
10
 *
11
 * - install-cli.php [-c|--continue-on-error] --git(-apps) <arguments>
12
 *   runs git with given arguments (in main- and) all app-dirs, e.g. tag -a 17.1.20190214 -m 'tagging release'
13
 *
14
 * EGroupware main directory should be either git cloned:
15
 *
16
 *	git clone [-b <branch>] https://github.com/EGroupware/egroupware [<target>]
17
 *
18
 * or created via composer create-project
19
 *
20
 *	composer create-project --prefer-source --keep-vcs egroupware/egroupware[:(dev-master|17.1.x-dev|<tag>)] <target>
21
 *
22
 * Both will create a git clone, which can be further updated by calling this tool without argument.
23
 *
24
 * We currently use 3 "channels":
25
 * - release: taged maintenance releases only eg. 17.1.20190214
26
 * - bugfix:  release-branch incl. latest bugfixes eg. 17.1 or 17.1.x-dev for composer
27
 * - master:  latest development for next release
28
 * To change the channel, call install-cli.php <channel-to-update-to>.
29
 *
30
 * This tool requires the following binaries installed at the usually places or in your path:
31
 * - php & git: apt/yum/zypper install php-cli git
32
 * - composer: see https://getcomposer.org/download/ for installation instructions
33
 * The following binaries are needed to minify JavaScript and CSS
34
 * - npm: apt/yum/zypper install npm
35
 * - grunt: npm install -g grunt-cli
36
 *
37
 * @link http://www.egroupware.org
38
 * @package api
39
 * @author Ralf Becker <[email protected]>
40
 * @copyright (c) 2019 by Ralf Becker <[email protected]>
41
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
42
 */
43
44
chdir(__DIR__);	// to enable relative pathes to work
45
46
if (php_sapi_name() !== 'cli')	// security precaution: forbit calling setup-cli as web-page
47
{
48
	die('<h1>install-cli.php must NOT be called as web-page --> exiting !!!</h1>');
49
}
50
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);
51
52
// parse arguments
53
$verbose = $use_prerelease = $run_git = $continue_on_error = false;
54
$composer_args = [];
55
56
$argv = $_SERVER['argv'];
57
$cmd  = array_shift($argv);
58
59
foreach($argv as $n => $arg)
60
{
61
	if ($arg[0] === '-')
62
	{
63
		switch($arg)
64
		{
65
			case '-v':
66
			case '--verbose':
67
				$verbose = true;
68
				unset($argv[$n]);
69
				break;
70
71
			case '--use-prerelease':
72
				$use_prerelease = true;
73
				unset($argv[$n]);
74
				break;
75
76
			case '-h':
77
			case '--help':
78
				usage();
79
80
			case '--git':
81
			case '--git-apps':
82
				$run_git = $arg;
83
				unset($argv[$n]);
84
				break 2;	// no further argument processing, as they are for git
85
86
			case '-c':
87
			case '--continue-on-error':
88
				$continue_on_error = true;
89
				unset($argv[$n]);
90
				break;
91
92
			default:	// pass unknown arguments to composer install
93
				$composer_args[] = $arg;
94
				unset($argv[$n]);
95
				break;
96
		}
97
	}
98
}
99
100
if (!$run_git && count($argv) > 1) usage("Too many arguments!");
101
102
function usage($err=null)
103
{
104
	global $cmd;
105
106
	if ($err)
107
	{
108
		echo "$err\n\n";
109
	}
110
	die("Usage:\t$cmd [-v|--verbose] [--use-prerelease] [<composer-args>] (master|bugfix|release|<branch>|<tag>)\n".
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
111
		"\t\nyou can use composer install arguments like: --ignore-platform-reqs --no-dev\n".
112
		"\t$cmd [-c|--continue-on-error] --git(-apps) <arguments>\n".
113
		"\truns git with given arguments (in main- and) all app-dirs, e.g. tag -a 17.1.20190214 -m 'tagging release'\n\n");
114
}
115
116
$bins = array(
117
	'php'      => PHP_BINARY,
118
	'git'      => '/usr/bin/git',
119
	'composer' => ['/usr/bin/composer', '/usr/bin/composer.phar'],
120
	// npm and grunt are no hard requirement and should be the last in the list!
121
	'npm'      => '/usr/bin/npm',
122
	'grunt'    => '/usr/bin/grunt',
123
);
124
125
// check if the necessary binaries are installed
126
foreach($bins as $name => $binaries)
127
{
128
	foreach((array)$binaries as $bin)
129
	{
130
		if (file_exists($bin) && is_executable($bin))
131
		{
132
			$bins[$name] = $$name = $bin;
133
			continue 2;
134
		}
135
	}
136
	$output = $ret = null;
137
	if (($bin = exec('which '.$name, $output, $ret)) && !$ret &&
138
		(file_exists($bin)) && is_executable($bin))
139
	{
140
		$bins[$name] = $$name = $bin;
141
	}
142
	// check if we can just run it, because it's in the path
143
	elseif (exec($name.' -v', $output, $ret) && !$ret)
144
	{
145
		$bins[$name] = $$name = $num;
146
	}
147
	else
148
	{
149
		$bins[$name] = $$name = false;
150
		error_log("Could not find $name command!");
151
		if (!in_array($name, ['npm','grunt']))
152
		{
153
			exit(1);
154
		}
155
		else
156
		{
157
			error_log("npm and grunt are required to minify JavaScript and CSS files to improve performance.");
158
			break;
159
		}
160
	}
161
}
162
163
if ($verbose) echo "Using following binaries: ".json_encode ($bins, JSON_UNESCAPED_SLASHES)."\n";
164
165
if (!extension_loaded('curl')) die("Required PHP extesion 'curl' missing! You need to install php-curl package.\n\n");
166
167
// check if we are on a git clone
168
$output = array();
169
if (!file_exists(__DIR__.'/.git') || !is_dir(__DIR__.'/.git'))
170
{
171
	error_log("Could not identify git branch (you need to use git clone or composer create-project --prefer-source --keep-vcs egroupware/egroupware)!");
172
	exit(1);
173
}
174
175
// should we only run a git command
176
if ($run_git)
177
{
178
	exit (run_git($argv, $run_git === '--git'));
179
}
180
181
if (!exec($git.' branch --no-color', $output, $ret) || $ret)
182
{
183
	foreach($output as $line)
184
	{
185
		error_log($line);
186
	}
187
	exit($ret);
188
}
189
foreach($output as $line)
190
{
191
	if ($line[0] == '*')
192
	{
193
		$branch = substr($line, 2);
194
		// are we on a tag
195
		if (preg_match('/^\(HEAD .* ([0-9.]+)\)$/', $branch, $matches))
196
		{
197
			$branch = $matches[1];
198
		}
199
		break;
200
	}
201
}
202
$channel = 'development';
203
if (preg_match('/^\d+\.\d+(\.\d{8})?/', $branch, $machtes))
204
{
205
	$channel = isset($matches[1]) ? 'release' : 'bugfix';
206
}
207
if ($verbose) echo "Currently using branch: $branch --> $channel channel\n";
208
209
if ($argv)
210
{
211
	$target = array_shift($argv);
212
213
	if ($target === 'release')
214
	{
215
		$target = get_latest_release($use_prerelease);
216
	}
217
	elseif ($target === 'bugfix')
218
	{
219
		$target = (string)(float)get_latest_release($use_prerelease);
220
	}
221
}
222
else
223
{
224
	$target = $branch;
225
226
	// find latest release
227
	if ($channel == 'release')
228
	{
229
		$target = get_latest_release($use_prerelease);
230
	}
231
}
232
233
// a branch update requires a composer install with --prefer-source
234
if (count(explode('.', $target)) < 2)
235
{
236
	$composer_args[] = '--prefer-source';
237
}
238
239
echo "Updating to: $target\n";
240
241
// Update EGroupware itself and further apps installed via git
242
$failed = array();
243
$succieded = 0;
244
foreach(scandir(__DIR__) as $dir)
245
{
246
	if ($dir !== '..' && file_exists(__DIR__.'/'.$dir.'/.git'))
247
		// these apps / dirs are managed by composer, no need to run manual updates
248
		//!in_array($dir, ['vendor', 'activesync', 'collabora', 'projectmanager', 'tracker']))
249
	{
250
		$cmd = "cd $dir ; $git stash -q ; ";
251
		// switch message about detached head off for release-channel/tags
252
		if (preg_match('/^\d+\.\d+\.\d{8}/', $target))
253
		{
254
			$cmd .= "$git config advice.detachedHead false ; ";
255
		}
256
		if ($branch != $target)
257
		{
258
			$cmd .= "$git checkout $target && ";
259
		}
260
		// no need to pull for release-channel/tags
261
		if (!preg_match('/^\d+\.\d+\.\d{8}/', $target))
262
		{
263
			$cmd .= "$git pull --rebase && ";
264
		}
265
		$cmd .= "(test -z \"$($git stash list)\" || $git stash pop)";
266
		if ($dir !== '.' && !$verbose)
267
		{
268
			echo $dir.': ';
269
		}
270
		run_cmd($cmd, $dir === '.' ? 'egroupware' : $dir);
271
	}
272
}
273
274
// update composer managed dependencies
275
$cmd = $composer.' install '.implode(' ', $composer_args);
276
run_cmd($cmd, 'composer');
277
278
// update npm dependencies and run grunt to minify javascript and css
279
if ($npm && $grunt)
280
{
281
	run_cmd($npm.' install', 'npm');
282
283
	run_cmd($grunt, 'grunt');
284
}
285
286
echo "\n$succieded tasks successful run".
287
	($failed ? ', '.count($failed).' failed: '.implode(', ', $failed) : '')."\n\n";
0 ignored issues
show
$failed is an empty array, thus is always false.
Loading history...
288
exit(count($failed));
289
290
/**
291
 * Run a command and collect number of succieded or failed command
292
 *
293
 * @param string $cmd comamnd to run
294
 * @param string $name task name to report on failure
295
 * @return int exit code of command
296
 */
297
function run_cmd($cmd, $name)
298
{
299
	global $verbose, $failed, $succieded;
300
301
	if ($verbose) echo "$cmd\n";
302
	$ret = null;
303
	system($cmd, $ret);
304
	if ($ret == 0)
305
	{
306
		$succieded++;
307
	}
308
	else
309
	{
310
		$failed[] = $name;
311
	}
312
	return $ret;
313
}
314
315
/**
316
 * Run git command with given arguments all app-dirs and (optional) install-dir
317
 *
318
 * cd and git command is echoed to stderr
319
 *
320
 * @param array $argv
321
 * @param booelan $main_too =true true: run in main-dir too, false: only app-dirs
0 ignored issues
show
The type booelan was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
322
 * @return int exit-code of last git command, breaks on first non-zero exit-code
323
 */
324
function run_git(array $argv, $main_too=true)
325
{
326
	global $git, $continue_on_error;
327
328
	$git_cmd = $git.' '.implode(' ', array_map('escapeshellarg', $argv));
329
330
	$ret = 0;
331
	foreach(scandir(__DIR__) as $dir)
332
	{
333
		if (!($dir === '..' || $dir === '.' && !$main_too ||
334
			!file_exists(__DIR__.'/'.$dir.'/.git')))
335
		{
336
			$cmd = ($dir !== '.' ? "cd $dir; " : '').$git_cmd;
337
338
			error_log("\n>>> ".$cmd."\n");
339
			system($cmd, $ret);
340
			// break if command is not successful, unless --continue-on-error
341
			if ($ret && !$continue_on_error) return $ret;
342
		}
343
	}
344
	return $ret;
345
}
346
347
/**
348
 * Get latest release
349
 *
350
 * @param boolean $prerelease =false include releases taged as prerelease
351
 * @param boolean $return_name =true true: just return name, false: full release object
352
 * @return array|string|null null if no release found
353
 */
354
function get_latest_release($prerelease=false, $return_name=true)
355
{
356
	foreach(github_api('/repos/egroupware/egroupware/releases', [], 'GET') as $release)
357
	{
358
		if ($prerelease || $release['prerelease'] === false)
359
		{
360
			return $return_name ? $release['tag_name'] : $release;
361
		}
362
	}
363
	return null;
364
}
365
366
/**
367
 * Sending a Github API request
368
 *
369
 * @param string $_url url of just path where to send request to (https://api.github.com is added automatic)
370
 * @param string|array $data payload, array get automatic added as get-parameter or json_encoded for POST
371
 * @param string $method ='POST'
372
 * @param string $upload =null path of file to upload, payload for request with $method='FILE'
373
 * @param string $content_type =null
374
 * @throws Exception
375
 * @return array with response
376
 */
377
function github_api($_url, $data, $method='POST', $upload=null, $content_type=null)
378
{
379
	global /*$config,*/ $verbose;
380
381
	$url = $_url[0] == '/' ? 'https://api.github.com'.$_url : $_url;
382
	$c = curl_init();
383
	curl_setopt($c, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
384
	//curl_setopt($c, CURLOPT_USERPWD, $config['github_user'].':'.$config['github_token']);
385
	curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
386
	curl_setopt($c, CURLOPT_USERAGENT, basename(__FILE__));
387
	curl_setopt($c, CURLOPT_TIMEOUT, 240);
388
	curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
389
390
	switch($method)
391
	{
392
		case 'POST':
393
			curl_setopt($c, CURLOPT_POST, true);
394
			if (is_array($data)) $data = json_encode($data, JSON_FORCE_OBJECT);
395
			curl_setopt($c, CURLOPT_POSTFIELDS, $data);
396
			break;
397
		case 'GET':
398
			if(count($data)) $url .= '?' . http_build_query($data);
399
			break;
400
		case 'FILE':
401
			curl_setopt($c, CURLOPT_HTTPHEADER, array("Content-type: $content_type"));
402
			curl_setopt($c, CURLOPT_POST, true);
403
			curl_setopt($c, CURLOPT_POSTFIELDS, file_get_contents($upload));
404
			if(count($data)) $url .= '?' . http_build_query($data);
405
			break;
406
		default:
407
			throw new Exception(__FUNCTION__.": Unknown/unimplemented method=$method!");
408
	}
409
	curl_setopt($c, CURLOPT_URL, $url);
410
411
	if (is_string($data)) $short_data = strlen($data) > 100 ? substr($data, 0, 100).' ...' : $data;
412
	if ($verbose) echo "Sending $method request to $url ".(isset($short_data)&&$method!='GET'?$short_data:'')."\n";
413
414
	if (($response = curl_exec($c)) === false)
415
	{
416
		// run failed request again to display response including headers
417
		curl_setopt($c, CURLOPT_HEADER, true);
418
		curl_setopt($c, CURLOPT_RETURNTRANSFER, false);
419
		curl_exec($c);
420
		throw new Exception("$method request to $url failed ".(isset($short_data)&&$method!='GET'?$short_data:''));
421
	}
422
423
	if ($verbose) echo (strlen($response) > 200 ? substr($response, 0, 200).' ...' : $response)."\n";
0 ignored issues
show
Are you sure strlen($response) > 200 ...0) . ' ...' : $response of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

423
	if ($verbose) echo (/** @scrutinizer ignore-type */ strlen($response) > 200 ? substr($response, 0, 200).' ...' : $response)."\n";
Loading history...
424
425
	curl_close($c);
426
427
	return json_decode($response, true);
428
}
429