Test Setup Failed
Push — 17.1 ( 4dae72...46295b )
by Ralf
09:40
created

install-cli.php (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 --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 = 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
			default:	// pass unknown arguments to composer install
87
				$composer_args[] = $arg;
88
				unset($argv[$n]);
89
				break;
90
		}
91
	}
92
}
93
94
if (!$run_git && count($argv) > 1) usage("Too many arguments!");
95
96 View Code Duplication
function usage($err=null)
97
{
98
	global $cmd;
99
100
	if ($err)
101
	{
102
		echo "$err\n\n";
103
	}
104
	die("Usage:\t$cmd [-v|--verbose] [--use-prerelease] [<composer-args>] (master|bugfix|release|<branch>|<tag>)\n".
105
		"\t\nyou can use composer install arguments like: --ignore-platform-reqs --no-dev\n".
106
		"\t$cmd --git(-apps) <arguments>\n".
107
		"\truns git with given arguments (in main- and) all app-dirs, e.g. tag -a 17.1.20190214 -m 'tagging release'\n\n");
108
}
109
110
$bins = array(
111
	'php'      => PHP_BINARY,
112
	'git'      => '/usr/bin/git',
113
	'composer' => ['/usr/bin/composer', '/usr/bin/composer.phar'],
114
	// npm and grunt are no hard requirement and should be the last in the list!
115
	'npm'      => '/usr/bin/npm',
116
	'grunt'    => '/usr/bin/grunt',
117
);
118
119
// check if the necessary binaries are installed
120
foreach($bins as $name => $binaries)
121
{
122
	foreach((array)$binaries as $bin)
123
	{
124
		if (file_exists($bin) && is_executable($bin))
125
		{
126
			$bins[$name] = $$name = $bin;
127
			continue 2;
128
		}
129
	}
130
	$output = $ret = null;
131
	if (($bin = exec('which '.$name, $output, $ret)) && !$ret &&
132
		(file_exists($bin)) && is_executable($bin))
133
	{
134
		$bins[$name] = $$name = $bin;
135
	}
136
	// check if we can just run it, because it's in the path
137
	elseif (exec($name.' -v', $output, $ret) && !$ret)
138
	{
139
		$bins[$name] = $$name = $num;
140
	}
141
	else
142
	{
143
		$bins[$name] = $$name = false;
144
		error_log("Could not find $name command!");
145
		if (!in_array($name, ['npm','grunt']))
146
		{
147
			exit(1);
148
		}
149
		else
150
		{
151
			error_log("npm and grunt are required to minify JavaScript and CSS files to improve performance.");
152
			break;
153
		}
154
	}
155
}
156
157
if ($verbose) echo "Using following binaries: ".json_encode ($bins, JSON_UNESCAPED_SLASHES)."\n";
158
159
if (!extension_loaded('curl')) die("Required PHP extesion 'curl' missing! You need to install php-curl package.\n\n");
160
161
// check if we are on a git clone
162
$output = array();
163
if (!file_exists(__DIR__.'/.git') || !is_dir(__DIR__.'/.git'))
164
{
165
	error_log("Could not identify git branch (you need to use git clone or composer create-project --prefer-source --keep-vcs egroupware/egroupware)!");
166
	exit(1);
167
}
168
169
// should we only run a git command
170
if ($run_git)
171
{
172
	exit (run_git($argv, $run_git === '--git'));
173
}
174
175
if (!exec($git.' branch --no-color', $output, $ret) || $ret)
176
{
177
	foreach($output as $line)
178
	{
179
		error_log($line);
180
	}
181
	exit($ret);
182
}
183
foreach($output as $line)
184
{
185
	if ($line[0] == '*')
186
	{
187
		$branch = substr($line, 2);
188
		// are we on a tag
189
		if (preg_match('/^\(HEAD .* ([0-9.]+)\)$/', $branch, $matches))
190
		{
191
			$branch = $matches[1];
192
		}
193
		break;
194
	}
195
}
196
$channel = 'development';
197
if (preg_match('/^\d+\.\d+(\.\d{8})?/', $branch, $machtes))
198
{
199
	$channel = isset($matches[1]) ? 'release' : 'bugfix';
200
}
201
if ($verbose) echo "Currently using branch: $branch --> $channel channel\n";
202
203
if ($argv)
204
{
205
	$target = array_shift($argv);
206
207
	if ($target === 'release')
208
	{
209
		$target = get_latest_release($use_prerelease);
210
	}
211
	elseif ($target === 'bugfix')
212
	{
213
		$target = (string)(float)get_latest_release($use_prerelease);
214
	}
215
}
216
else
217
{
218
	$target = $branch;
219
220
	// find latest release
221
	if ($channel == 'release')
222
	{
223
		$target = get_latest_release($use_prerelease);
224
	}
225
}
226
227
echo "Updating to: $target\n";
228
229
// Update EGroupware itself and further apps installed via git
230
$failed = array();
231
$succieded = 0;
232
foreach(scandir(__DIR__) as $dir)
233
{
234
	if ($dir !== '..' && file_exists(__DIR__.'/'.$dir.'/.git') &&
235
		// these apps / dirs are managed by composer, no need to run manual updates
236
		!in_array($dir, ['vendor', 'activesync', 'collabora', 'projectmanager', 'tracker']))
237
	{
238
		$cmd = "cd $dir ; $git stash -q ; ";
239
		// switch message about detached head off for release-channel/tags
240
		if (preg_match('/^\d+\.\d+\.\d{8}/', $target))
241
		{
242
			$cmd .= "$git config advice.detachedHead false ; ";
243
		}
244
		if ($branch != $target)
245
		{
246
			$cmd .= "$git checkout $target && ";
247
		}
248
		// no need to pull for release-channel/tags
249
		if (!preg_match('/^\d+\.\d+\.\d{8}/', $target))
250
		{
251
			$cmd .= "$git pull --rebase && ";
252
		}
253
		$cmd .= "(test -z \"$($git stash list)\" || $git stash pop)";
254
		if ($verbose)
255
		{
256
			echo "$cmd\n";
257
		}
258
		elseif ($dir !== '.')
259
		{
260
			echo $dir.': ';
261
		}
262
		run_cmd($cmd, $dir === '.' ? 'egroupware' : $dir);
0 ignored issues
show
$dir === '.' ? 'egroupware' : $dir cannot be passed to run_cmd() as the parameter $output expects a reference.
Loading history...
263
	}
264
}
265
266
// update composer managed dependencies
267
$cmd = $composer.' install '.implode(' ', $composer_args);
268
run_cmd($cmd, 'composer');
0 ignored issues
show
'composer' cannot be passed to run_cmd() as the parameter $output expects a reference.
Loading history...
269
270
// update npm dependencies and run grunt to minify javascript and css
271
if ($npm && $grunt)
272
{
273
	run_cmd($npm.' install', 'npm');
0 ignored issues
show
'npm' cannot be passed to run_cmd() as the parameter $output expects a reference.
Loading history...
274
275
	run_cmd($grunt, 'grunt');
0 ignored issues
show
'grunt' cannot be passed to run_cmd() as the parameter $output expects a reference.
Loading history...
276
}
277
278
echo "\n$succieded tasks successful run".
279
	($failed ? ', '.count($failed).' failed: '.implode(', ', $failed) : '')."\n\n";
280
exit(count($failed));
281
282
/**
283
 * Run a command and collect number of succieded or failed command
284
 *
285
 * @param string $cmd comamnd to run
286
 * @param string $name task name to report on failure
287
 * @return int exit code of command
288
 */
289
function run_cmd($cmd, $name)
0 ignored issues
show
The function run_cmd() has been defined more than once; this definition is ignored, only the first definition in doc/rpm-build/checkout-build-archives.php (L1344-1366) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
290
{
291
	global $verbose, $failed, $succieded;
292
293
	if ($verbose) echo "$cmd\n";
294
	$ret = null;
295
	system($cmd, $ret);
296
	if ($ret == 0)
0 ignored issues
show
It seems like you are loosely comparing $ret of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
297
	{
298
		$succieded++;
299
	}
300
	else
301
	{
302
		$failed[] = $name;
303
	}
304
	return $ret;
305
}
306
307
/**
308
 * Run git command with given arguments all app-dirs and (optional) install-dir
309
 *
310
 * cd and git command is echoed to stderr
311
 *
312
 * @param array $argv
313
 * @param booelan $main_too =true true: run in main-dir too, false: only app-dirs
314
 * @return int exit-code of last git command, breaks on first non-zero exit-code
315
 */
316
function run_git(array $argv, $main_too=true)
317
{
318
	global $git;
319
320
	$git_cmd = $git.' '.implode(' ', array_map('escapeshellarg', $argv));
321
322
	$ret = 0;
323
	foreach(scandir(__DIR__) as $dir)
324
	{
325
		if (!($dir === '..' || $dir === '.' && !$main_too ||
326
			!file_exists(__DIR__.'/'.$dir.'/.git')))
327
		{
328
			$cmd = ($dir !== '.' ? "cd $dir; " : '').$git_cmd;
329
330
			error_log("\n>>> ".$cmd."\n");
331
			system($cmd, $ret);
332
			// break if command is not successful
333
			if ($ret) return $ret;
334
		}
335
	}
336
	return $ret;
337
}
338
339
/**
340
 * Get latest release
341
 *
342
 * @param boolean $prerelease =false include releases taged as prerelease
343
 * @param boolean $return_name =true true: just return name, false: full release object
344
 * @return array|string|null null if no release found
345
 */
346
function get_latest_release($prerelease=false, $return_name=true)
347
{
348
	foreach(github_api('/repos/egroupware/egroupware/releases', [], 'GET') as $release)
349
	{
350
		if ($prerelease || $release['prerelease'] === false)
351
		{
352
			return $return_name ? $release['tag_name'] : $release;
353
		}
354
	}
355
	return null;
356
}
357
358
/**
359
 * Sending a Github API request
360
 *
361
 * @param string $_url url of just path where to send request to (https://api.github.com is added automatic)
362
 * @param string|array $data payload, array get automatic added as get-parameter or json_encoded for POST
363
 * @param string $method ='POST'
364
 * @param string $upload =null path of file to upload, payload for request with $method='FILE'
365
 * @param string $content_type =null
366
 * @throws Exception
367
 * @return array with response
368
 */
369
function github_api($_url, $data, $method='POST', $upload=null, $content_type=null)
370
{
371
	global /*$config,*/ $verbose;
372
373
	$url = $_url[0] == '/' ? 'https://api.github.com'.$_url : $_url;
374
	$c = curl_init();
375
	curl_setopt($c, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
376
	//curl_setopt($c, CURLOPT_USERPWD, $config['github_user'].':'.$config['github_token']);
377
	curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
378
	curl_setopt($c, CURLOPT_USERAGENT, basename(__FILE__));
379
	curl_setopt($c, CURLOPT_TIMEOUT, 240);
380
	curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
381
382 View Code Duplication
	switch($method)
383
	{
384
		case 'POST':
385
			curl_setopt($c, CURLOPT_POST, true);
386
			if (is_array($data)) $data = json_encode($data, JSON_FORCE_OBJECT);
387
			curl_setopt($c, CURLOPT_POSTFIELDS, $data);
388
			break;
389
		case 'GET':
390
			if(count($data)) $url .= '?' . http_build_query($data);
391
			break;
392
		case 'FILE':
393
			curl_setopt($c, CURLOPT_HTTPHEADER, array("Content-type: $content_type"));
394
			curl_setopt($c, CURLOPT_POST, true);
395
			curl_setopt($c, CURLOPT_POSTFIELDS, file_get_contents($upload));
396
			if(count($data)) $url .= '?' . http_build_query($data);
397
			break;
398
		default:
399
			throw new Exception(__FUNCTION__.": Unknown/unimplemented method=$method!");
400
	}
401
	curl_setopt($c, CURLOPT_URL, $url);
402
403 View Code Duplication
	if (is_string($data)) $short_data = strlen($data) > 100 ? substr($data, 0, 100).' ...' : $data;
404 View Code Duplication
	if ($verbose) echo "Sending $method request to $url ".(isset($short_data)&&$method!='GET'?$short_data:'')."\n";
405
406 View Code Duplication
	if (($response = curl_exec($c)) === false)
407
	{
408
		// run failed request again to display response including headers
409
		curl_setopt($c, CURLOPT_HEADER, true);
410
		curl_setopt($c, CURLOPT_RETURNTRANSFER, false);
411
		curl_exec($c);
412
		throw new Exception("$method request to $url failed ".(isset($short_data)&&$method!='GET'?$short_data:''));
413
	}
414
415 View Code Duplication
	if ($verbose) echo (strlen($response) > 200 ? substr($response, 0, 200).' ...' : $response)."\n";
416
417
	curl_close($c);
418
419
	return json_decode($response, true);
420
}
421