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