|
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". |
|
|
|
|
|
|
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"; |
|
|
|
|
|
|
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 |
|
|
|
|
|
|
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"; |
|
|
|
|
|
|
424
|
|
|
|
|
425
|
|
|
curl_close($c); |
|
426
|
|
|
|
|
427
|
|
|
return json_decode($response, true); |
|
428
|
|
|
} |
|
429
|
|
|
|
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.