1#!/usr/bin/env php
2<?php
3/*
4   +----------------------------------------------------------------------+
5   | Copyright (c) The PHP Group                                          |
6   +----------------------------------------------------------------------+
7   | This source file is subject to version 3.01 of the PHP license,      |
8   | that is bundled with this package in the file LICENSE, and is        |
9   | available through the world-wide-web at the following url:           |
10   | https://www.php.net/license/3_01.txt                                 |
11   | If you did not receive a copy of the PHP license and are unable to   |
12   | obtain it through the world-wide-web, please send a note to          |
13   | license@php.net so we can mail you a copy immediately.               |
14   +----------------------------------------------------------------------+
15   | Authors: Kalle Sommer Nielsen <kalle@php.net>                        |
16   +----------------------------------------------------------------------+
17*/
18
19/* $Id: ae7a3a987c0a9a6a0d4d07cc79eed0afa15e79dc $ */
20
21/* {{{ error */
22function error($message) {
23    printf('Error: %s%s', $message, PHP_EOL);
24    exit;
25}
26/* }}} */
27
28/* {{{ print_help */
29function print_help() {
30    if (PHP_OS_FAMILY != 'Windows') {
31        $file_prefix = './';
32        $make_prefix = '';
33    } else {
34        $file_prefix = '';
35        $make_prefix = 'n';
36    }
37
38    echo <<<HELP
39WHAT IT IS
40
41  It's a tool for automatically creating the basic framework for a PHP extension.
42
43HOW TO USE IT
44
45  Very simple. First, change to the ext/ directory of the PHP sources. Then run
46  the following
47
48    php ext_skel.php --ext extension_name
49
50  and everything you need will be placed in directory ext/extension_name.
51
52  If you don't need to test the existence of any external header files,
53  libraries or functions in them, the extension is ready to be compiled in PHP.
54  To compile the extension run the following:
55
56    cd extension_name
57    phpize
58    {$file_prefix}configure
59    {$make_prefix}make
60
61  Don't forget to run tests once the compilation is done:
62
63    {$make_prefix}make test
64
65  Alternatively, to compile extension in the PHP:
66
67    cd /path/to/php-src
68    {$file_prefix}buildconf
69    {$file_prefix}configure --enable-extension_name
70    {$make_prefix}make
71    {$make_prefix}make test TESTS=ext/extension_name/tests
72
73  The definition of PHP_extension_NAME_VERSION will be present in the
74  php_extension_name.h and injected into the zend_extension_entry definition.
75  This is required by the PECL website for the version string conformity checks
76  against package.xml
77
78SOURCE AND HEADER FILE NAME
79
80  The ext_skel.php script generates 'extension_name.c' and 'php_extension_name.h'
81  as the main source and header files. Keep these names.
82
83  extension functions (User functions) must be named
84
85  extension_name_function()
86
87  When you need to expose extension functions to other extensions, expose
88  functions strictly needed by others. Exposed internal function must be named
89
90  php_extension_name_function()
91
92  See also CODING_STANDARDS.md.
93
94OPTIONS
95
96  php ext_skel.php --ext <name> [--experimental] [--author <name>]
97                   [--dir <path>] [--std] [--onlyunix]
98                   [--onlywindows] [--help]
99
100  --ext <name>          The name of the extension defined as <name>
101  --experimental        Passed if this extension is experimental, this creates
102                        the EXPERIMENTAL file in the root of the extension
103  --author <name>       Your name, this is used if --std is passed and for the
104                        CREDITS file
105  --dir <path>          Path to the directory for where extension should be
106                        created. Defaults to the directory of where this script
107                        lives
108  --std                 If passed, the standard header used in extensions that
109                        is included in the core, will be used
110  --onlyunix            Only generate configure scripts for Unix
111  --onlywindows         Only generate configure scripts for Windows
112  --help                This help
113
114HELP;
115    exit;
116}
117/* }}} */
118
119/* {{{ task */
120function task($label, $callback) {
121    printf('%s... ', $label);
122
123    $callback();
124
125    printf('done%s', PHP_EOL);
126}
127/* }}} */
128
129/* {{{ print_success */
130function print_success() {
131    global $options;
132
133    if (PHP_OS_FAMILY != 'Windows') {
134        $file_prefix = './';
135        $make_prefix = '';
136    } else {
137        $file_prefix = '';
138        $make_prefix = 'n';
139    }
140
141    printf('%1$sSuccess. The extension is now ready to be compiled. To do so, use the%s', PHP_EOL);
142    printf('following steps:%1$s%1$s', PHP_EOL);
143    printf('cd %s%s%s', $options['dir'], $options['ext'], PHP_EOL);
144    printf('phpize%s', PHP_EOL);
145    printf('%sconfigure%s', $file_prefix, PHP_EOL);
146    printf('%smake%2$s%2$s', $make_prefix, PHP_EOL);
147    printf('Don\'t forget to run tests once the compilation is done:%s', PHP_EOL);
148    printf('%smake test%2$s%2$s', $make_prefix, PHP_EOL);
149    printf('Thank you for using PHP!%s', PHP_EOL);
150}
151/* }}} */
152
153/* {{{ process_args */
154function process_args($argv, $argc) {
155    $options = [
156            'unix'		=> true,
157            'windows' 	=> true,
158            'ext' 		=> '',
159            'dir'		=> __DIR__ . DIRECTORY_SEPARATOR,
160            'skel' 		=> __DIR__ . DIRECTORY_SEPARATOR . 'skeleton' . DIRECTORY_SEPARATOR,
161            'author'	=> false,
162            'experimental'	=> false,
163            'std'		=> false
164            ];
165
166    for($i = 1; $i < $argc; ++$i)
167    {
168        $val = $argv[$i];
169
170        if($val[0] != '-' || $val[1] != '-')
171        {
172            continue;
173        }
174
175        switch($opt = strtolower(substr($val, 2)))
176        {
177            case 'help': {
178                print_help();
179            }
180            case 'onlyunix': {
181                $options['windows'] = false;
182            }
183            break;
184            case 'onlywindows': {
185                $options['unix'] = false;
186            }
187            break;
188            case 'experimental': {
189                $options['experimental'] = true;
190            }
191            break;
192            case 'std': {
193                $options['std'] = true;
194            }
195            break;
196            case 'ext':
197            case 'dir':
198            case 'author': {
199                if (!isset($argv[$i + 1]) || ($argv[$i + 1][0] == '-' && $argv[$i + 1][1] == '-')) {
200                    error('Argument "' . $val . '" expects a value, none passed');
201                } else if ($opt == 'dir' && empty($argv[$i + 1])) {
202                    continue 2;
203                }
204
205                $options[$opt] = ($opt == 'dir' ? realpath($argv[$i + 1]) . DIRECTORY_SEPARATOR : $argv[$i + 1]);
206            }
207            break;
208            default: {
209                error('Unsupported argument "' . $val . '" passed');
210            }
211        }
212    }
213
214    if (empty($options['ext'])) {
215        error('No extension name passed, use "--ext <name>"');
216    } else if (!$options['unix'] && !$options['windows']) {
217        error('Cannot pass both --onlyunix and --onlywindows');
218    } else if (!is_dir($options['skel'])) {
219        error('The skeleton directory was not found');
220    }
221
222    // Validate extension name
223    if (!preg_match('/^[a-z][a-z0-9_]+$/i', $options['ext'])) {
224        error('Invalid extension name. Valid names start with a letter,'
225            .' followed by any number of letters, numbers, or underscores.'
226            .' Using only lower case letters is preferred.');
227    }
228
229    $options['ext'] = str_replace(['\\', '/'], '', strtolower($options['ext']));
230
231    return $options;
232}
233/* }}} */
234
235/* {{{ process_source_tags */
236function process_source_tags($file, $short_name) {
237    global $options;
238
239    $source = file_get_contents($file);
240
241    if ($source === false) {
242        error('Unable to open file for reading: ' . $short_name);
243    }
244
245    $source = str_replace('%EXTNAME%', $options['ext'], $source);
246    $source = str_replace('%EXTNAMECAPS%', strtoupper($options['ext']), $source);
247
248    if (strpos($short_name, '.c') !== false || strpos($short_name, '.h') !== false) {
249        static $header;
250
251        if (!$header) {
252            if ($options['std']) {
253                $author_len = strlen($options['author']);
254                $credits = $options['author'] . ($author_len && $author_len <= 60 ? str_repeat(' ', 60 - $author_len) : '');
255
256                $header = <<<"HEADER"
257/*
258   +----------------------------------------------------------------------+
259   | Copyright (c) The PHP Group                                          |
260   +----------------------------------------------------------------------+
261   | This source file is subject to version 3.01 of the PHP license,      |
262   | that is bundled with this package in the file LICENSE, and is        |
263   | available through the world-wide-web at the following url:           |
264   | https://www.php.net/license/3_01.txt                                 |
265   | If you did not receive a copy of the PHP license and are unable to   |
266   | obtain it through the world-wide-web, please send a note to          |
267   | license@php.net so we can mail you a copy immediately.               |
268   +----------------------------------------------------------------------+
269   | Author: $credits |
270   +----------------------------------------------------------------------+
271*/
272HEADER;
273            } else {
274                if ($options['author']) {
275                    $header = sprintf('/* %s extension for PHP (c) %d %s */', $options['ext'], date('Y'), $options['author']);
276                } else {
277                    $header = sprintf('/* %s extension for PHP */', $options['ext']);
278                }
279            }
280        }
281
282        $source = str_replace('%HEADER%', $header, $source);
283    }
284
285    if (!file_put_contents($file, $source)) {
286        error('Unable to save contents to file: ' . $short_name);
287    }
288}
289/* }}} */
290
291/* {{{ copy_config_scripts */
292function copy_config_scripts() {
293    global $options;
294
295    $files = [];
296
297    if ($options['unix']) {
298        $files[] = 'config.m4';
299    }
300
301    if ($options['windows']) {
302        $files[] = 'config.w32';
303    }
304
305    $files[] = '.gitignore';
306
307    foreach($files as $config_script) {
308        $new_config_script = $options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . $config_script;
309
310        if (!copy($options['skel'] . $config_script . '.in', $new_config_script)) {
311            error('Unable to copy config script: ' . $config_script);
312        }
313
314        process_source_tags($new_config_script, $config_script);
315    }
316}
317/* }}} */
318
319/* {{{ copy_sources */
320function copy_sources() {
321    global $options;
322
323    $files = [
324            'skeleton.c'		=> $options['ext'] . '.c',
325            'skeleton.stub.php'	=> $options['ext'] . '.stub.php',
326            'php_skeleton.h'	=> 'php_' . $options['ext'] . '.h',
327            'skeleton_arginfo.h' => $options['ext'] . '_arginfo.h'
328            ];
329
330    foreach ($files as $src_file => $dst_file) {
331        if (!copy($options['skel'] . $src_file, $options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . $dst_file)) {
332            error('Unable to copy source file: ' . $src_file);
333        }
334
335        process_source_tags($options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . $dst_file, $dst_file);
336    }
337}
338/* }}} */
339
340/* {{{ copy_tests */
341function copy_tests() {
342    global $options;
343
344    $test_files = glob($options['skel'] . 'tests/*', GLOB_MARK);
345
346    if (!$test_files) {
347        return;
348    }
349
350    foreach ($test_files as $test) {
351        if (is_dir($test)) {
352            continue;
353        }
354
355        $new_test = str_replace([$options['skel'], '/'], ['', DIRECTORY_SEPARATOR], $test);
356
357        if (!copy($test, $options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . $new_test)) {
358            error('Unable to copy file: ' . $new_test);
359        }
360
361        process_source_tags($options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . $new_test, $new_test);
362    }
363}
364/* }}} */
365
366
367if (PHP_SAPI != 'cli') {
368    error('This script is only suited for CLI');
369}
370
371if ($argc < 1) {
372    print_help();
373    exit;
374}
375
376$options = process_args($argv, $argc);
377
378if (!$options['dir'] || !is_dir($options['dir'])) {
379    error('The selected output directory does not exist');
380} else if (is_dir($options['dir'] . $options['ext'])) {
381    error('There is already a folder named "' . $options['ext'] . '" in the output directory');
382} else if (!mkdir($options['dir'] . $options['ext'])) {
383    error('Unable to create extension directory in the output directory');
384} else if (!mkdir($options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . 'tests')) {
385    error('Unable to create the tests directory');
386}
387
388if ($options['experimental']) {
389    print('Creating EXPERIMENTAL... ');
390
391    if (file_put_contents($options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . 'EXPERIMENTAL', '') === false) {
392        error('Unable to create the EXPERIMENTAL file');
393    }
394
395    printf('done%s', PHP_EOL);
396}
397
398if (!empty($options['author'])) {
399    print('Creating CREDITS... ');
400
401    if (!file_put_contents($options['dir'] . $options['ext'] . DIRECTORY_SEPARATOR . 'CREDITS', $options['ext'] . PHP_EOL . $options['author'])) {
402        error('Unable to create the CREDITS file');
403    }
404
405    printf('done%s', PHP_EOL);
406}
407
408date_default_timezone_set('UTC');
409
410task('Copying config scripts', 'copy_config_scripts');
411task('Copying sources', 'copy_sources');
412task('Copying tests', 'copy_tests');
413
414print_success();
415