fix packaging relative source directories
[pharext/pharext] / src / pharext / Packager.php
1 <?php
2
3 namespace pharext;
4
5 use Phar;
6
7 /**
8 * The extension packaging command executed by bin/pharext
9 */
10 class Packager implements Command
11 {
12 use Cli\Command;
13
14 /**
15 * Extension source directory
16 * @var pharext\SourceDir
17 */
18 private $source;
19
20 /**
21 * Cleanups
22 * @var array
23 */
24 private $cleanup = [];
25
26 /**
27 * Create the command
28 */
29 public function __construct() {
30 $this->args = new Cli\Args([
31 ["h", "help", "Display this help",
32 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG|Cli\Args::HALT],
33 ["v", "verbose", "More output",
34 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG],
35 ["q", "quiet", "Less output",
36 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG],
37 ["n", "name", "Extension name",
38 Cli\Args::REQUIRED|Cli\Args::SINGLE|Cli\Args::REQARG],
39 ["r", "release", "Extension release version",
40 Cli\Args::REQUIRED|Cli\Args::SINGLE|Cli\Args::REQARG],
41 ["s", "source", "Extension source directory",
42 Cli\Args::REQUIRED|Cli\Args::SINGLE|Cli\Args::REQARG],
43 ["g", "git", "Use `git ls-tree` to determine file list",
44 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG],
45 ["b", "branch", "Checkout this tag/branch",
46 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::REQARG],
47 ["p", "pecl", "Use PECL package.xml to determine file list, name and release",
48 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG],
49 ["d", "dest", "Destination directory",
50 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::REQARG,
51 "."],
52 ["z", "gzip", "Create additional PHAR compressed with gzip",
53 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG],
54 ["Z", "bzip", "Create additional PHAR compressed with bzip",
55 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG],
56 ["S", "sign", "Sign the PHAR with a private key",
57 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::REQARG],
58 ["E", "zend", "Mark as Zend Extension",
59 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG],
60 [null, "signature", "Show pharext signature",
61 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG|Cli\Args::HALT],
62 [null, "license", "Show pharext license",
63 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG|Cli\Args::HALT],
64 [null, "version", "Show pharext version",
65 Cli\Args::OPTIONAL|Cli\Args::SINGLE|Cli\Args::NOARG|Cli\Args::HALT],
66 ]);
67 }
68
69 /**
70 * Perform cleaniup
71 */
72 function __destruct() {
73 foreach ($this->cleanup as $cleanup) {
74 $cleanup->run();
75 }
76 }
77
78 /**
79 * @inheritdoc
80 * @see \pharext\Command::run()
81 */
82 public function run($argc, array $argv) {
83 $errs = [];
84 $prog = array_shift($argv);
85 foreach ($this->args->parse(--$argc, $argv) as $error) {
86 $errs[] = $error;
87 }
88
89 if ($this->args["help"]) {
90 $this->header();
91 $this->help($prog);
92 exit;
93 }
94 try {
95 foreach (["signature", "license", "version"] as $opt) {
96 if ($this->args[$opt]) {
97 printf("%s\n", $this->metadata($opt));
98 exit;
99 }
100 }
101 } catch (\Exception $e) {
102 $this->error("%s\n", $e->getMessage());
103 exit(self::EARGS);
104 }
105
106 try {
107 /* source needs to be evaluated before Cli\Args validation,
108 * so Cli\Args does not complain about missing arguments,
109 * which come from SourceDir::getPackageInfo()
110 */
111 $this->loadSource();
112 } catch (\Exception $e) {
113 $errs[] = $e->getMessage();
114 }
115
116 foreach ($this->args->validate() as $error) {
117 $errs[] = $error;
118 }
119
120 if ($errs) {
121 if (!$this->args["quiet"]) {
122 $this->header();
123 }
124 foreach ($errs as $err) {
125 $this->error("%s\n", $err);
126 }
127 printf("\n");
128 if (!$this->args["quiet"]) {
129 $this->help($prog);
130 }
131 exit(self::EARGS);
132 }
133
134 $this->createPackage();
135 }
136
137 /**
138 * Download remote source
139 * @param string $source
140 * @return string local source
141 */
142 private function download($source) {
143 if ($this->args->git) {
144 $task = new Task\GitClone($source, $this->args->branch);
145 } else {
146 /* print newline only once */
147 $done = false;
148 $task = new Task\StreamFetch($source, function($bytes_pct) use(&$done) {
149 if (!$done) {
150 $this->info(" %3d%% [%s>%s] \r",
151 floor($bytes_pct*100),
152 str_repeat("=", round(50*$bytes_pct)),
153 str_repeat(" ", round(50*(1-$bytes_pct)))
154 );
155 if ($bytes_pct == 1) {
156 $done = true;
157 $this->info("\n");
158 }
159 }
160 });
161 }
162 $local = $task->run($this->verbosity());
163
164 $this->cleanup[] = new Task\Cleanup($local);
165 return $local;
166 }
167
168 /**
169 * Extract local archive
170 * @param stirng $source
171 * @return string extracted directory
172 */
173 private function extract($source) {
174 try {
175 $task = new Task\Extract($source);
176 $dest = $task->run($this->verbosity());
177 } catch (\Exception $e) {
178 if (false === strpos($e->getMessage(), "checksum mismatch")) {
179 throw $e;
180 }
181 $dest = (new Task\PaxFixup($source))->run($this->verbosity());
182 }
183
184 $this->cleanup[] = new Task\Cleanup($dest);
185 return $dest;
186 }
187
188 /**
189 * Localize a possibly remote source
190 * @param string $source
191 * @return string local source directory
192 */
193 private function localize($source) {
194 if (!stream_is_local($source) || ($this->args->git && isset($this->args->branch))) {
195 $source = $this->download($source);
196 $this->cleanup[] = new Task\Cleanup($source);
197 }
198 if (!$real = realpath($source)) {
199 $error = "Cannot find source '$source'";
200 if ($this->args->git) {
201 $error .= "; did you forget to specify --branch for a remote git source?";
202 }
203 throw new Exception($error);
204 }
205 $source = $real;
206 if (!is_dir($real)) {
207 $source = $this->extract($real);
208 $this->cleanup[] = new Task\Cleanup($source);
209
210 if (!$this->args->git) {
211 $source = (new Task\PeclFixup($source))->run($this->verbosity());
212 }
213 }
214 return $source;
215 }
216
217 /**
218 * Load the source dir
219 * @throws \pharext\Exception
220 */
221 private function loadSource(){
222 if ($this->args["source"]) {
223 $source = $this->localize($this->args["source"]);
224
225 if ($this->args["pecl"]) {
226 $this->source = new SourceDir\Pecl($source);
227 } elseif ($this->args["git"]) {
228 $this->source = new SourceDir\Git($source);
229 } elseif (is_file("$source/pharext_package.php")) {
230 $this->source = include "$source/pharext_package.php";
231 } else {
232 $this->source = new SourceDir\Basic($source);
233 }
234
235 if (!$this->source instanceof SourceDir) {
236 throw new Exception("Unknown source dir $source");
237 }
238
239 foreach ($this->source->getPackageInfo() as $key => $val) {
240 /* do not override command line arguments */
241 if (!isset($this->args->$key)) {
242 $this->args->$key = $val;
243 }
244 }
245 }
246 }
247
248 /**
249 * Creates the extension phar
250 */
251 private function createPackage() {
252 try {
253 $meta = array_merge(Metadata::all(), [
254 "name" => $this->args->name,
255 "release" => $this->args->release,
256 "license" => $this->source->getLicense(),
257 "type" => $this->args->zend ? "zend_extension" : "extension",
258 ]);
259 $file = (new Task\PharBuild($this->source, __DIR__."/../pharext_installer.php", $meta))->run($this->verbosity());
260 } catch (\Exception $e) {
261 $this->error("%s\n", $e->getMessage());
262 exit(self::EBUILD);
263 }
264
265 try {
266 if ($this->args->sign) {
267 $this->info("Using private key to sign phar ...\n");
268 $pass = (new Task\Askpass)->run($this->verbosity());
269 $sign = new Task\PharSign($file, $this->args->sign, $pass);
270 $pkey = $sign->run($this->verbosity());
271 }
272
273 } catch (\Exception $e) {
274 $this->error("%s\n", $e->getMessage());
275 exit(self::ESIGN);
276 }
277
278 if ($this->args->gzip) {
279 try {
280 $gzip = (new Task\PharCompress($file, Phar::GZ))->run();
281 $move = new Task\PharRename($gzip, $this->args->dest, $this->args->name ."-". $this->args->release);
282 $name = $move->run($this->verbosity());
283
284 $this->info("Created gzipped phar %s\n", $name);
285
286 if ($this->args->sign) {
287 $sign = new Task\PharSign($name, $this->args->sign, $pass);
288 $sign->run($this->verbosity())->exportPublicKey($name.".pubkey");
289 }
290
291 } catch (\Exception $e) {
292 $this->warn("%s\n", $e->getMessage());
293 }
294 }
295
296 if ($this->args->bzip) {
297 try {
298 $bzip = (new Task\PharCompress($file, Phar::BZ2))->run();
299 $move = new Task\PharRename($bzip, $this->args->dest, $this->args->name ."-". $this->args->release);
300 $name = $move->run($this->verbosity());
301
302 $this->info("Created bzipped phar %s\n", $name);
303
304 if ($this->args->sign) {
305 $sign = new Task\PharSign($name, $this->args->sign, $pass);
306 $sign->run($this->verbosity())->exportPublicKey($name.".pubkey");
307 }
308
309 } catch (\Exception $e) {
310 $this->warn("%s\n", $e->getMessage());
311 }
312 }
313
314 try {
315 $move = new Task\PharRename($file, $this->args->dest, $this->args->name ."-". $this->args->release);
316 $name = $move->run($this->verbosity());
317
318 $this->info("Created executable phar %s\n", $name);
319
320 if (isset($pkey)) {
321 $pkey->exportPublicKey($name.".pubkey");
322 }
323
324 } catch (\Exception $e) {
325 $this->error("%s\n", $e->getMessage());
326 exit(self::EBUILD);
327 }
328 }
329 }