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