├── .gitattributes ├── tests ├── Bob │ └── Test │ │ ├── fixtures │ │ ├── file_task │ │ │ ├── in1.txt │ │ │ ├── in2.txt │ │ │ └── out.txt │ │ ├── Bobfile.php │ │ ├── bob_config.php │ │ └── tasks │ │ │ ├── bar.php │ │ │ └── foo.php │ │ ├── TaskRegistryTest.php │ │ ├── TaskTest.php │ │ ├── TaskLibraryTest.php │ │ ├── FileTaskTest.php │ │ └── ApplicationTest.php └── bootstrap.php ├── .gitignore ├── bin ├── bob └── bootstrap.php ├── .travis.yml ├── lib ├── Bob │ ├── TaskLibraryInterface.php │ ├── TaskRegistry.php │ ├── TaskInvocationChain.php │ ├── Library │ │ ├── TestingLibrary.php │ │ └── ComposerLibrary.php │ ├── FileTask.php │ ├── ConfigFile.php │ ├── Task.php │ ├── Cli.php │ ├── Application.php │ └── Dsl.php └── Bob.php ├── phpunit.xml.dist ├── composer.json ├── LICENSE.txt ├── bob_config.php ├── README.md └── composer.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | /bin/bob.phar export-ignore 2 | -------------------------------------------------------------------------------- /tests/Bob/Test/fixtures/file_task/in1.txt: -------------------------------------------------------------------------------- 1 | Foo 2 | 3 | -------------------------------------------------------------------------------- /tests/Bob/Test/fixtures/file_task/in2.txt: -------------------------------------------------------------------------------- 1 | Bar 2 | 3 | -------------------------------------------------------------------------------- /tests/Bob/Test/fixtures/file_task/out.txt: -------------------------------------------------------------------------------- 1 | Foo 2 | Bar 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /phpunit.xml 2 | /vendor/ 3 | /coverage/ 4 | /composer.phar 5 | -------------------------------------------------------------------------------- /bin/bob: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | 9 | lib 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/Bob/TaskRegistry.php: -------------------------------------------------------------------------------- 1 | name, $task); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /bin/bootstrap.php: -------------------------------------------------------------------------------- 1 | application; 22 | 23 | exit($cli->run()); 24 | 25 | -------------------------------------------------------------------------------- /tests/Bob/Test/TaskRegistryTest.php: -------------------------------------------------------------------------------- 1 | registry = new TaskRegistry; 14 | } 15 | 16 | function testOffsetGetReturnsNullWhenNoTask() 17 | { 18 | $this->assertNull($this->registry['foo']); 19 | } 20 | 21 | function testRegisterTreatsNamePropertyAsKey() 22 | { 23 | $task = (object) array( 24 | 'name' => 'foo' 25 | ); 26 | 27 | $this->registry[] = $task; 28 | 29 | $this->assertEquals($task, $this->registry['foo']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Bob/Test/TaskTest.php: -------------------------------------------------------------------------------- 1 | actions[] = function() use (&$invoked) { 15 | $invoked++; 16 | }; 17 | 18 | $t->invoke(); 19 | $t->invoke(); 20 | 21 | $this->assertEquals(1, $invoked); 22 | 23 | $t->reenable(); 24 | $t->invoke(); 25 | $this->assertEquals(2, $invoked); 26 | } 27 | 28 | function isNeededReturnsTrue() 29 | { 30 | $t = new Task('foo', new Application); 31 | $this->assertEquals(true, $t->isNeeded()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/Bob/TaskInvocationChain.php: -------------------------------------------------------------------------------- 1 | objectMap = new SplObjectStorage; 15 | $this->stack = new SplStack; 16 | } 17 | 18 | function push($task) 19 | { 20 | $this->stack->push($task); 21 | $this->objectMap->attach($task); 22 | } 23 | 24 | function pop() 25 | { 26 | $task = $this->stack->pop(); 27 | $this->objectMap->detach($task); 28 | 29 | return $task; 30 | } 31 | 32 | function has($task) 33 | { 34 | return $this->objectMap->contains($task); 35 | } 36 | 37 | function getIterator() 38 | { 39 | return $this->stack; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/Bob/Library/TestingLibrary.php: -------------------------------------------------------------------------------- 1 | fileTask('phpunit.xml', array($app['testing.dist_config']), function($task) { 26 | copy($task->prerequisites->current(), $task->name); 27 | }); 28 | 29 | $app->task('test', array('phpunit.xml'), function() use ($app) { 30 | b\sh($app['testing.phpunit_bin'], array('fail_on_error' => true)); 31 | 32 | })->description = "Run test suite"; 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chh/bob", 3 | "description": "A tiny and messy build automation tool for PHP projects.", 4 | "keywords": [ 5 | "build" 6 | ], 7 | "license": "MIT", 8 | "homepage": "https://github.com/CHH/Bob", 9 | "authors": [ 10 | { 11 | "name": "Christoph Hochstrasser", 12 | "email": "christoph.hochstrasser@gmail.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.3.3", 17 | "symfony/finder": "~2.0", 18 | "symfony/process": "~2.0", 19 | "chh/optparse": "~0.1", 20 | "chh/fileutils": "~1.0", 21 | "monolog/monolog": "~1.0", 22 | "pimple/pimple": "~1.0", 23 | "chh/itertools": "~1.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "~3.7" 27 | }, 28 | "autoload": { 29 | "psr-0": { 30 | "Bob\\": "lib/" 31 | } 32 | }, 33 | "bin": [ 34 | "bin/bob" 35 | ], 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "1.0.x-dev" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Christoph Hochstrasser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/Bob/FileTask.php: -------------------------------------------------------------------------------- 1 | name) 17 | or $this->getTimestamp() > @filemtime($this->name); 18 | } 19 | 20 | # Internal: Returns the timestamp when the prerequisites were last modified. 21 | # 22 | # Returns the time as Unix Epoche represented by an Integer. 23 | function getTimestamp() 24 | { 25 | $lastModifiedTimes = iterator_to_array(itertools\filter(itertools\map( 26 | function($file) { 27 | if (file_exists($file)) { 28 | return @filemtime($file); 29 | } 30 | }, 31 | $this->prerequisites 32 | ))); 33 | 34 | if ($lastModifiedTimes) { 35 | return max($lastModifiedTimes); 36 | } 37 | 38 | return 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/Bob/ConfigFile.php: -------------------------------------------------------------------------------- 1 | task('composer.phar', function($task) { 18 | if (file_exists($task->name)) { 19 | return true; 20 | } 21 | 22 | $src = fopen('http://getcomposer.org/composer.phar', 'rb'); 23 | $dest = fopen($task->name, 'wb'); 24 | 25 | stream_copy_to_stream($src, $dest); 26 | }); 27 | 28 | $app->task('composer:install', array('composer.phar'), function() { 29 | b\php(array('composer.phar', 'install'), null, array('failOnError' => true)); 30 | }); 31 | 32 | $app->task('composer:update', array('composer.phar'), function() { 33 | b\php(array('composer.phar', 'update'), null, array('failOnError' => true)); 34 | }); 35 | 36 | $app->fileTask('composer.lock', array('composer.phar', 'composer.json'), function($task) use ($app) { 37 | $app->task('composer:update')->invoke(); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Bob/Test/TaskLibraryTest.php: -------------------------------------------------------------------------------- 1 | application = new \Bob\Application; 15 | 16 | $this->application['log'] = $this->application->share(function() { 17 | $log = new Logger('bob'); 18 | $log->pushHandler(new TestHandler); 19 | 20 | return $log; 21 | }); 22 | 23 | $this->application['config.file'] = null; 24 | } 25 | 26 | function testRegister() 27 | { 28 | $lib = $this->getMock('\\Bob\\TaskLibraryInterface'); 29 | 30 | $lib->expects($this->once())->method('register') 31 | ->with($this->equalTo($this->application)); 32 | 33 | $this->application->register($lib); 34 | } 35 | 36 | function testBoot() 37 | { 38 | $lib = $this->getMock('\\Bob\\TaskLibraryInterface'); 39 | 40 | $lib->expects($this->once())->method('register') 41 | ->with($this->equalTo($this->application)); 42 | 43 | $lib->expects($this->once())->method('boot') 44 | ->with($this->equalTo($this->application)); 45 | 46 | $this->application->register($lib); 47 | 48 | $this->application->init(); 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /tests/Bob/Test/FileTaskTest.php: -------------------------------------------------------------------------------- 1 | getFixturesDir().'/foo.txt', new Application); 18 | 19 | $this->assertTrue($t->isNeeded()); 20 | } 21 | 22 | function testIsNeededWhenPrerequisitesAreNewerThanTarget() 23 | { 24 | $t = new FileTask($this->getFixturesDir().'/out.txt', new Application); 25 | $t->enhance(array( 26 | $this->getFixturesDir().'/in1.txt', 27 | $this->getFixturesDir().'/in2.txt') 28 | ); 29 | 30 | touch($this->getFixturesDir().'/in1.txt'); 31 | touch($this->getFixturesDir().'/out.txt', strtotime('-1 minute')); 32 | 33 | $this->assertTrue($t->isNeeded()); 34 | } 35 | 36 | function testIsNotNeededWhenTargetNewerThanPrerequisites() 37 | { 38 | $t = new FileTask($this->getFixturesDir().'/out.txt', new Application); 39 | $t->enhance(array( 40 | $this->getFixturesDir().'/in1.txt', 41 | $this->getFixturesDir().'/in2.txt') 42 | ); 43 | 44 | touch($this->getFixturesDir().'/in1.txt', strtotime('-1 minute')); 45 | touch($this->getFixturesDir().'/in2.txt', strtotime('-1 minute')); 46 | touch($this->getFixturesDir().'/out.txt'); 47 | 48 | $this->assertFalse($t->isNeeded()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bob_config.php: -------------------------------------------------------------------------------- 1 | in(array('lib', 'bin', 'vendor')); 11 | 12 | register(new \Bob\Library\TestingLibrary, array( 13 | 'testing.dist_config' => 'phpunit.xml.dist' 14 | )); 15 | 16 | register(new \Bob\Library\ComposerLibrary); 17 | 18 | # The "default" task is invoked when there's no 19 | # task explicitly given on the command line. 20 | task('default', array('phar')); 21 | 22 | # Note: All file paths used here should be relative to the project 23 | # directory. Bob automatically sets the current working directory 24 | # to the path where the `bob_config.php` resides. 25 | 26 | desc('Compiles a executable, standalone PHAR file'); 27 | task('phar', array('composer.lock', 'test', 'bin/bob.phar')); 28 | 29 | task('clean', function() { 30 | file_exists('bin/bob.phar') and unlink('bin/bob.phar'); 31 | }); 32 | 33 | task('release', function($task) { 34 | if (!$version = @$_ENV['version'] ?: @$_SERVER['BOB_VERSION']) { 35 | failf('No version given'); 36 | } 37 | 38 | if (substr($version, 0, 1) === 'v') { 39 | $version = substr($version, 1); 40 | } 41 | 42 | if (!@$_ENV['skip-branching']) { 43 | sh(sprintf('git checkout -b "release/%s"', $version)); 44 | } 45 | 46 | info(sprintf('----> Setting version to "v%s"', $version)); 47 | 48 | $contents = file_get_contents('lib/Bob.php'); 49 | 50 | $contents = preg_replace( 51 | '/(\s*)(const VERSION = ".+")/', 52 | sprintf('\1const VERSION = "v%s"', $version), 53 | $contents 54 | ); 55 | 56 | file_put_contents('lib/Bob.php', $contents); 57 | }); 58 | 59 | fileTask('bin/bob.phar', $pharFiles, function($task) { 60 | if (file_exists($task->name)) { 61 | unlink($task->name); 62 | } 63 | 64 | $stub = <<<'EOF' 65 | #!/usr/bin/env php 66 | projectDirectory; 76 | 77 | $phar = new \Phar($task->name, 0, basename($task->name)); 78 | $phar->startBuffering(); 79 | 80 | foreach ($task->prerequisites as $file) { 81 | $file = (string) $file; 82 | $phar->addFile($file, Path::relativize($file, $projectDir)); 83 | } 84 | 85 | $phar->setStub($stub); 86 | $phar->stopBuffering(); 87 | 88 | chmod($task->name, 0555); 89 | 90 | println(sprintf( 91 | 'Regenerated Archive "%s" with %d entries', basename($task->name), count($phar) 92 | )); 93 | unset($phar); 94 | }); 95 | 96 | desc('Does a system install of Bob, by default to /usr/local/bin'); 97 | task('install', array('bin/bob.phar'), function($task) { 98 | $prefix = getenv('PREFIX') ?: '/usr/local'; 99 | 100 | $success = copy('bin/bob.phar', "$prefix/bin/bob"); 101 | chmod("$prefix/bin/bob", 0755); 102 | 103 | println(sprintf('Installed the `bob` executable in %s.', $prefix)); 104 | }); 105 | 106 | desc('Removes the `bob` excutable from the PREFIX'); 107 | task('uninstall', function($task) { 108 | $prefix = getenv("PREFIX") ?: "/usr/local"; 109 | 110 | if (!file_exists("$prefix/bin/bob")) { 111 | println("Seems that bob is not installed. Aborting.", STDERR); 112 | return 1; 113 | } 114 | 115 | unlink("$prefix/bin/bob") and println("Erased bob successfully from $prefix"); 116 | }); 117 | 118 | task("foo", function() {}); 119 | -------------------------------------------------------------------------------- /tests/Bob/Test/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | application = new \Bob\Application; 15 | 16 | $this->application['log'] = $this->application->share(function() { 17 | $log = new Logger('bob'); 18 | $log->pushHandler(new TestHandler); 19 | 20 | return $log; 21 | }); 22 | 23 | \Bob::$application = $this->application; 24 | } 25 | 26 | function testTaskShort() 27 | { 28 | $action = function() { 29 | return "bar"; 30 | }; 31 | 32 | $task = $this->application->task("foo", $action); 33 | 34 | $this->assertTrue($this->application->taskDefined("foo")); 35 | $this->assertInstanceOf("\\Bob\\Task", $task); 36 | 37 | $this->assertEquals("foo", $task->name); 38 | $this->assertNull($task->prerequisites); 39 | $this->assertEquals($action, $task->actions[0]); 40 | } 41 | 42 | function testTaskLong() 43 | { 44 | $action = function() {}; 45 | $deps = new \ArrayIterator(array('bar')); 46 | 47 | $task = $this->application->task('foo', $deps, $action); 48 | 49 | $this->assertTrue($this->application->taskDefined('foo')); 50 | 51 | $this->assertEquals($deps, $task->prerequisites); 52 | $this->assertEquals($action, $task->actions[0]); 53 | } 54 | 55 | function testTaskReturnsExistingTaskWhenAlreadyDefined() 56 | { 57 | $task = $this->application->task('foo', function() {}); 58 | 59 | $this->assertEquals($task, $this->application->task('foo')); 60 | } 61 | 62 | /** 63 | * @expectedException \InvalidArgumentException 64 | */ 65 | function testExecuteThrowsExceptionWhenTaskNotFound() 66 | { 67 | $this->application->execute('foo'); 68 | } 69 | 70 | function testExecute() 71 | { 72 | $invoked = 0; 73 | 74 | $this->application->task('foo', array('bar'), function() use (&$invoked) { 75 | $invoked++; 76 | }); 77 | 78 | $this->application->task('bar', function() use (&$invoked) { 79 | $invoked++; 80 | }); 81 | 82 | $this->application->execute('foo'); 83 | 84 | $this->assertEquals(2, $invoked); 85 | } 86 | 87 | function testExecuteSetsWorkingDirectoryToProjectDirectory() 88 | { 89 | $cwd = ""; 90 | 91 | $this->application->task('foo', function() use (&$cwd) { 92 | $cwd = getcwd(); 93 | }); 94 | 95 | $this->application->projectDirectory = __DIR__; 96 | 97 | $this->application->execute('foo'); 98 | 99 | $this->assertEquals(__DIR__, $cwd); 100 | } 101 | 102 | function testLoadDefaultRootConfig() 103 | { 104 | $cwd = getcwd(); 105 | chdir(__DIR__ . '/fixtures'); 106 | 107 | $this->application->init(); 108 | 109 | $this->assertTrue($this->application->taskDefined('baz')); 110 | $this->assertContains(__DIR__ . '/fixtures/bob_config.php', $this->application->loadedConfigs); 111 | 112 | chdir($cwd); 113 | } 114 | 115 | function testLoadCustomRootConfig() 116 | { 117 | $cwd = getcwd(); 118 | chdir(__DIR__ . '/fixtures'); 119 | 120 | $this->application['config.file'] = "Bobfile.php"; 121 | 122 | $this->application->init(); 123 | 124 | $this->assertTrue($this->application->taskDefined('foobar')); 125 | $this->assertContains(__DIR__ . '/fixtures/Bobfile.php', $this->application->loadedConfigs); 126 | 127 | chdir($cwd); 128 | } 129 | 130 | function testMultipleRootConfigNamesChoosesFirstFound() 131 | { 132 | $cwd = getcwd(); 133 | chdir(__DIR__ . '/fixtures'); 134 | 135 | $this->application['config.file'] = array("Bobfile.php", "bob_config.php"); 136 | 137 | $this->application->init(); 138 | 139 | $this->assertTrue($this->application->taskDefined('foobar')); 140 | $this->assertContains(__DIR__ . '/fixtures/Bobfile.php', $this->application->loadedConfigs); 141 | 142 | chdir($cwd); 143 | } 144 | 145 | function testLoadConfigsFromLoadPath() 146 | { 147 | $cwd = getcwd(); 148 | chdir(__DIR__ . '/fixtures'); 149 | 150 | $this->application['config.load_path'] = array(__DIR__ . '/fixtures/tasks'); 151 | $this->application->init(); 152 | 153 | $this->assertTrue($this->application->taskDefined('foo')); 154 | $this->assertTrue($this->application->taskDefined('bar')); 155 | 156 | $this->assertContains(__DIR__ . '/fixtures/tasks/foo.php', $this->application->loadedConfigs); 157 | $this->assertContains(__DIR__ . '/fixtures/tasks/bar.php', $this->application->loadedConfigs); 158 | 159 | chdir($cwd); 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /lib/Bob/Task.php: -------------------------------------------------------------------------------- 1 | name = $name; 40 | $this->application = $application; 41 | 42 | $this->description = TaskRegistry::$lastDescription; 43 | TaskRegistry::$lastDescription = ''; 44 | 45 | $this->clear(); 46 | } 47 | 48 | # Child classes of Task should put here their custom logic to determine 49 | # if the task should do something. See the FileTask class for an 50 | # example of this. 51 | # 52 | # Returns TRUE if the task should be run, FALSE otherwise. 53 | function isNeeded() 54 | { 55 | return true; 56 | } 57 | 58 | # Public: Collects all dependencies and invokes the task if it's 59 | # needed. 60 | # 61 | # Returns the callback's return value. 62 | function invoke() 63 | { 64 | if (!$this->enable) { 65 | if ($this->application->trace) { 66 | $this->application['log']->debug("{$this->inspect()} is not enabled"); 67 | } 68 | return; 69 | } 70 | 71 | if (!$this->reenable and $this->application['invocation_chain']->has($this)) { 72 | return; 73 | } 74 | 75 | if (!$this->application->forceRun and !$this->isNeeded()) { 76 | $this->application->trace and $this->application['log']->debug("Skipping {$this->inspect()}"); 77 | return; 78 | } 79 | 80 | $this->application['invocation_chain']->push($this); 81 | 82 | if ($this->application->trace) { 83 | $this->application['log']->debug("Invoke {$this->inspect()}"); 84 | } 85 | 86 | $app = $this->application; 87 | 88 | itertools\walk( 89 | itertools\filter(itertools\to_iterator($this->prerequisites), function($p) use ($app) { 90 | return $app->taskDefined((string) $p); 91 | }), 92 | function($p) use ($app) { 93 | $app['tasks'][(string) $p]->invoke(); 94 | } 95 | ); 96 | 97 | if ($this->prerequisites !== null) { 98 | $this->prerequisites->rewind(); 99 | } 100 | 101 | $this->execute(); 102 | $this->reenable = false; 103 | } 104 | 105 | # Internal: Executes all actions. 106 | # 107 | # Returns nothing. 108 | function execute() 109 | { 110 | foreach ($this->actions as $action) { 111 | call_user_func($action, $this); 112 | } 113 | } 114 | 115 | # Clears all actions and prerequisites. 116 | # 117 | # Returns nothing. 118 | function clear() 119 | { 120 | $this->actions = new \SplDoublyLinkedList; 121 | $this->prerequisites = null; 122 | } 123 | 124 | function reenable() 125 | { 126 | $this->reenable = true; 127 | } 128 | 129 | function enhance($deps = null, $action = null) 130 | { 131 | if ($deps) { 132 | if ($this->prerequisites instanceof \Iterator) { 133 | $p = new \AppendIterator; 134 | $p->append($this->prerequisites); 135 | $p->append(itertools\to_iterator($deps)); 136 | $this->prerequisites = $p; 137 | } else { 138 | $this->prerequisites = itertools\to_iterator($deps); 139 | } 140 | } 141 | 142 | if (is_callable($action)) { 143 | $this->actions->push($action); 144 | } 145 | } 146 | 147 | function getTaskPrerequisites() 148 | { 149 | $app = $this->application; 150 | 151 | return itertools\filter( 152 | itertools\to_iterator($this->prerequisites), 153 | function($task) use ($app) { 154 | return $app->taskDefined($task); 155 | } 156 | ); 157 | } 158 | 159 | function inspect() 160 | { 161 | $prereqs = join(', ', iterator_to_array($this->getTaskPrerequisites())); 162 | $class = get_class($this); 163 | return "<$class {$this->name} => $prereqs>"; 164 | } 165 | 166 | function __toString() 167 | { 168 | return $this->name; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/Bob/Cli.php: -------------------------------------------------------------------------------- 1 | application = new Application; 21 | 22 | $this->opts = new Optparse\Parser; 23 | $this->opts 24 | ->addFlag('init', array("alias" => '-i')) 25 | ->addFlag('version') 26 | ->addFlag('help', array("alias" => '-h')) 27 | ->addFlag('tasks', array("alias" => '-t')) 28 | ->addFlag('chdir', array("alias" => '-C', "has_value" => true)) 29 | ->addFlag('verbose', array("alias" => '-v')) 30 | ->addFlagVar('all', $this->showAllTasks, array("alias" => '-A')) 31 | ->addFlagVar('trace', $this->trace, array("alias" => '-T')) 32 | ->addFlagVar('force', $this->forceRun, array("alias" => '-f')) 33 | ; 34 | } 35 | 36 | function run($argv = null) 37 | { 38 | try { 39 | $this->opts->parse($argv); 40 | } catch (Optparse\Exception $e) { 41 | fwrite(STDERR, "{$e->getMessage()}\n\n"); 42 | fwrite(STDERR, $this->formatUsage()); 43 | return 1; 44 | } 45 | 46 | $this->application['log.verbose'] = (bool) $this->opts['verbose']; 47 | 48 | $this->application->trace = $this->trace; 49 | $this->application->forceRun = $this->forceRun; 50 | 51 | if ($this->opts['version']) { 52 | printf("Bob %s\n", \Bob::VERSION); 53 | return 0; 54 | } 55 | 56 | if ($this->opts["init"]) { 57 | return $this->initProject() ? 0 : 1; 58 | } 59 | 60 | if ($this->opts["help"]) { 61 | fwrite(STDERR, $this->formatUsage()); 62 | return 0; 63 | } 64 | 65 | if ($dir = $this->opts["chdir"]) { 66 | if (!is_dir($dir)) { 67 | $this->logger()->err(sprintf('Directory not found: "%s"', $dir)); 68 | return 1; 69 | } 70 | 71 | $this->logger()->info(sprintf('Changing working directory to "%s"', realpath($dir))); 72 | 73 | chdir($dir); 74 | } 75 | 76 | $this->application['config.load_path'] = array_merge($this->application['config.load_path'], explode(':', @$_SERVER['BOB_CONFIG_PATH'])); 77 | 78 | try { 79 | $this->application->init(); 80 | } catch (\Exception $e) { 81 | fwrite(STDERR, $e); 82 | return 127; 83 | } 84 | 85 | if ($this->opts["tasks"]) { 86 | fwrite(STDERR, $this->formatTasksAndDescriptions()); 87 | return 0; 88 | } 89 | 90 | $this->withErrorHandling(array($this, 'runTasks')); 91 | } 92 | 93 | function logger() 94 | { 95 | return $this->application['log']; 96 | } 97 | 98 | function withErrorHandling($callback) 99 | { 100 | try { 101 | call_user_func($callback); 102 | return 0; 103 | } catch (\Exception $e) { 104 | $this->logger()->err(sprintf( 105 | "Build failed: %s (use --trace to get a stack trace)", $e->getMessage()) 106 | ); 107 | 108 | if ($this->trace) { 109 | $this->logger()->info($e->getTraceAsString()); 110 | } 111 | 112 | return 1; 113 | } 114 | } 115 | 116 | protected function runTasks() 117 | { 118 | return $this->application->execute($this->collectTasks()); 119 | } 120 | 121 | protected function collectTasks() 122 | { 123 | $tasks = array(); 124 | $args = $this->opts->args(); 125 | 126 | foreach ($args as $arg) { 127 | if (preg_match('/^(\w+)=(.*)$/', $arg, $matches)) { 128 | $this->application->env[$matches[1]] = trim($matches[2], '"'); 129 | continue; 130 | } 131 | 132 | $tasks[] = $arg; 133 | } 134 | 135 | if (empty($tasks)) { 136 | $tasks += array('default'); 137 | } 138 | 139 | return $tasks; 140 | } 141 | 142 | protected function formatUsage() 143 | { 144 | $version = \Bob::VERSION; 145 | $bin = basename($_SERVER['SCRIPT_NAME']); 146 | 147 | return <<: 174 | Changes the working directory to before running tasks. 175 | -T|--trace: 176 | Logs trace messages to STDERR 177 | -f|--force: 178 | Force to run all tasks, even if they're not needed 179 | -v|--verbose: 180 | Be more verbose. 181 | -h|--help: 182 | Displays this message 183 | 184 | 185 | HELPTEXT; 186 | } 187 | 188 | protected function formatTasksAndDescriptions() 189 | { 190 | $tasks = $this->application['tasks']->getArrayCopy(); 191 | ksort($tasks); 192 | 193 | $text = ''; 194 | $text .= "(in {$this->application->projectDirectory})\n"; 195 | 196 | foreach ($tasks as $name => $task) { 197 | if ($name === 'default' || (!$task->description && !$this->showAllTasks)) { 198 | continue; 199 | } 200 | 201 | if ($task instanceof FileTask) { 202 | $text .= "File => {$task->name}"; 203 | } else { 204 | $text .= "bob {$task->name}"; 205 | } 206 | 207 | $text .= "\n"; 208 | 209 | if ($task->description) { 210 | $text .= "\t{$task->description}\n"; 211 | } 212 | } 213 | 214 | return $text; 215 | } 216 | 217 | protected function initProject() 218 | { 219 | if (file_exists("bob_config.php")) { 220 | fwrite(STDERR, "Project already has a bob_config.php\n"); 221 | return false; 222 | } 223 | 224 | $config = <<<'EOF' 225 | share(function() { 42 | return new TaskRegistry; 43 | }); 44 | 45 | $this['config.load_path'] = array('./bob_tasks'); 46 | $this['config.file'] = 'bob_config.php'; 47 | $this['default_task_class'] = "\\Bob\\Task"; 48 | 49 | $this['task_factory'] = $this->protect(function($name) use ($app) { 50 | $action = null; 51 | $prerequisites = null; 52 | $class = $app['default_task_class']; 53 | 54 | foreach (array_filter(array_slice(func_get_args(), 1)) as $arg) { 55 | switch (true) { 56 | case is_callable($arg): 57 | $action = $arg; 58 | break; 59 | case is_string($arg) and class_exists($arg): 60 | $class = $arg; 61 | break; 62 | case is_array($arg): 63 | case ($arg instanceof \Traversable): 64 | case ($arg instanceof \Iterator): 65 | $prerequisites = $arg; 66 | break; 67 | } 68 | } 69 | 70 | if (empty($name)) { 71 | throw new \InvalidArgumentException('Name cannot be empty'); 72 | } 73 | 74 | if ($app->taskDefined($name)) { 75 | $task = $app['tasks'][$name]; 76 | } else { 77 | $task = new $class($name, $app); 78 | $app->defineTask($task); 79 | } 80 | 81 | $task->enhance($prerequisites, $action); 82 | 83 | return $task; 84 | }); 85 | 86 | $this['log.verbose'] = false; 87 | 88 | $this['log'] = $this->share(function() use ($app) { 89 | $log = new Logger("bob"); 90 | 91 | $stderrHandler = new StreamHandler(STDERR, $app['log.verbose'] ? Logger::DEBUG : Logger::WARNING); 92 | $stderrHandler->setFormatter(new LineFormatter("%channel%: [%level_name%] %message%" . PHP_EOL)); 93 | 94 | $log->pushHandler($stderrHandler); 95 | 96 | return $log; 97 | }); 98 | 99 | $this['invocation_chain'] = $this->share(function() { 100 | return new TaskInvocationChain; 101 | }); 102 | } 103 | 104 | function register(TaskLibraryInterface $taskLib, array $parameters = array()) 105 | { 106 | $taskLib->register($this); 107 | $this->taskLibraries[] = $taskLib; 108 | 109 | foreach ($parameters as $param => $value) { 110 | $this[$param] = $value; 111 | } 112 | 113 | return $this; 114 | } 115 | 116 | function init() 117 | { 118 | $this->loadConfig(); 119 | 120 | foreach ($this->taskLibraries as $taskLib) { 121 | $taskLib->boot($this); 122 | } 123 | } 124 | 125 | function task($name, $prerequisites = null, $action = null) 126 | { 127 | return $this['task_factory']($name, $prerequisites, $action); 128 | } 129 | 130 | function fileTask($target, $prerequisites, $action) 131 | { 132 | return $this['task_factory']($target, $prerequisites, $action, "\\Bob\\FileTask"); 133 | } 134 | 135 | function execute($tasks) 136 | { 137 | $this->prepareEnv(); 138 | 139 | $tasks = (array) $tasks; 140 | $start = microtime(true); 141 | 142 | foreach ($tasks as $taskName) { 143 | if (!$task = $this['tasks'][$taskName]) { 144 | throw new \InvalidArgumentException(sprintf('Task "%s" not found.', $taskName)); 145 | } 146 | 147 | $this['log']->info(sprintf( 148 | 'Running Task "%s"', $taskName 149 | )); 150 | 151 | if ($this->projectDirectory) { 152 | Path::chdir($this->projectDirectory, array($task, 'invoke')); 153 | } else { 154 | $task->invoke(); 155 | } 156 | } 157 | 158 | $this['log']->info(sprintf('Build finished in %f seconds', microtime(true) - $start)); 159 | } 160 | 161 | function taskDefined($task) 162 | { 163 | if (is_object($task) and !empty($task->name)) { 164 | $task = $task->name; 165 | } 166 | 167 | return (bool) $this['tasks'][$task]; 168 | } 169 | 170 | function defineTask($task) 171 | { 172 | $this['tasks'][] = $task; 173 | return $this; 174 | } 175 | 176 | protected function prepareEnv() 177 | { 178 | $_ENV = array_merge($_ENV, $this->env); 179 | } 180 | 181 | # Internal: Looks up the build config files from the root of the project 182 | # and from the search dir in `./bob_tasks`. Build Config files contain 183 | # the task definitions. 184 | # 185 | # Returns nothing. 186 | protected function loadConfig() 187 | { 188 | $rootConfigPath = false; 189 | 190 | foreach ((array) $this['config.file'] as $file) { 191 | $rootConfigPath = ConfigFile::findConfigFile($file, getcwd()); 192 | 193 | if (false !== $rootConfigPath) { 194 | break; 195 | } 196 | } 197 | 198 | if (false === $rootConfigPath) { 199 | $this['log']->err(sprintf( 200 | "Filesystem boundary reached, none of %s found.\n", 201 | json_encode((array) $this['config.file']) 202 | )); 203 | return false; 204 | } 205 | 206 | $this->loadConfigFile($rootConfigPath); 207 | 208 | # Save the original working directory, the working directory 209 | # gets set to the project directory while running tasks. 210 | $this->originalDirectory = getcwd(); 211 | 212 | # The project dir is the directory of the root config. 213 | $this->projectDirectory = dirname($rootConfigPath); 214 | 215 | # Search for additional configs in the config load paths 216 | $configLoadPath = array_filter($this['config.load_path'], 'is_dir'); 217 | 218 | if ($configLoadPath) { 219 | $cwd = getcwd(); 220 | chdir($this->projectDirectory); 221 | 222 | $finder = Finder::create() 223 | ->files()->name("*.php") 224 | ->in($configLoadPath); 225 | 226 | foreach ($finder as $file) { 227 | $this->loadConfigFile($file); 228 | } 229 | 230 | chdir($cwd); 231 | } 232 | 233 | return true; 234 | } 235 | 236 | # Load config file in its own scope 237 | protected function loadConfigFile($file) 238 | { 239 | if (in_array($file, $this->loadedConfigs)) { 240 | $this['log']->info(sprintf('Skipping: Already loaded config "%s"', $file)); 241 | return; 242 | } 243 | 244 | $file = (string) $file; 245 | $app = $this; 246 | 247 | $config = function() use ($file, $app) { 248 | include($file); 249 | }; 250 | 251 | $config(); 252 | 253 | $this['log']->info(sprintf('Loaded config "%s"', $file)); 254 | $this->loadedConfigs[] = $file; 255 | } 256 | } 257 | 258 | -------------------------------------------------------------------------------- /lib/Bob/Dsl.php: -------------------------------------------------------------------------------- 1 | info($msg); 48 | } 49 | 50 | function register(TaskLibraryInterface $taskLib, array $parameters = array()) 51 | { 52 | return \Bob::$application->register($taskLib, $parameters); 53 | } 54 | 55 | # Public: Defines the callback as a task with the given name. 56 | # 57 | # name - Task Name. 58 | # prerequisites - List of Dependency names. 59 | # callback - The task's action, can be any callback. 60 | # 61 | # Examples 62 | # 63 | # task('hello', function() { 64 | # echo "Hello World\n"; 65 | # }); 66 | # 67 | # Returns nothing. 68 | function task($name, $prerequisites = null, $callback = null) 69 | { 70 | return \Bob::$application->task($name, $prerequisites, $callback); 71 | } 72 | 73 | # Public: Config file function for creating a task which is only run 74 | # when the target file does not exist, or the prerequisites were modified. 75 | # 76 | # target - Filename of the resulting file, this is set as task name. Use 77 | # paths relative to the CWD (the CWD is always set to the root 78 | # of your project for you). 79 | # prerequisites - List of files which are needed to generate the target. The callback 80 | # which generates the target is only run when one of this files is newer 81 | # than the target file. You can access this list from within the task via 82 | # the task's `prerequisites` property. 83 | # callback - Place your logic needed to generate the target here. It's only run when 84 | # the prerequisites were modified or the target does not exist. 85 | # 86 | # Returns a Task instance. 87 | function fileTask($target, $prerequisites = array(), $callback) 88 | { 89 | return \Bob::$application->fileTask($target, $prerequisites, $callback); 90 | } 91 | 92 | # Copies the file only when it doesn't exists or was updated. 93 | # 94 | # source - Source file. This file is watched for changes. 95 | # dest - Destination. 96 | # 97 | # Returns a Task instance. 98 | function copyTask($from, $to) 99 | { 100 | return fileTask($to, array($from), function($task) { 101 | logger()->info("copyTask('{$task->prerequisites[0]}' => '{$task->name}')"); 102 | 103 | if (false === copy($task->prerequisites[0], $task->name)) { 104 | fail("Failed copying '{$task->prerequisites[0]}' => '{$task->name}'"); 105 | } 106 | }); 107 | } 108 | 109 | function directoryTask($directory, $mode = 0777) 110 | { 111 | return task($directory, function($task) use ($directory, $mode) { 112 | if (!is_dir($directory)) { 113 | mkdir($directory, $mode, true); 114 | } 115 | }); 116 | } 117 | 118 | # Public: Defines the description of the subsequent task. 119 | # 120 | # desc - Description text, should explain in plain sentences 121 | # what the task does. 122 | # 123 | # Examples 124 | # 125 | # desc('Says Hello World to NAME'); 126 | # task('greet', function($task) { 127 | # $operands = Bob::$application->opts->getOperands(); 128 | # $name = $operands[1]; 129 | # 130 | # echo "Hello World $name!\n"; 131 | # }); 132 | # 133 | # Returns nothing. 134 | function desc($desc) 135 | { 136 | TaskRegistry::$lastDescription = $desc; 137 | } 138 | 139 | # Public: Appends an End-Of-Line character to the given 140 | # text and writes it to a stream. 141 | # 142 | # line - Text to write. 143 | # stream - Resource to write the text to (optional). By 144 | # default the text is printed to STDOUT via `echo` 145 | # 146 | # Examples 147 | # 148 | # # Print something to STDERR (uses fwrite) 149 | # println('Error', STDERR); 150 | # 151 | # Returns Nothing. 152 | function println($line, $stream = null) 153 | { 154 | $line .= PHP_EOL; 155 | 156 | if (is_resource($stream)) { 157 | fwrite($stream, $line); 158 | } else { 159 | echo "$line"; 160 | } 161 | } 162 | 163 | # Public: Renders a PHP template. 164 | # 165 | # file - Template file, this must be a valid PHP file. 166 | # 167 | # Examples 168 | # 169 | # # template.phtml 170 | # Hello 171 | # 172 | # # test.php 173 | # $t = template('template.phtml'); 174 | # echo $t(array('name' => 'Christoph')); 175 | # # => Hello Christoph 176 | # 177 | # Returns an anonymous function of the variables, which returns 178 | # the rendered String. 179 | function template($file) 180 | { 181 | if (!file_exists($file)) { 182 | throw \InvalidArgumentException(sprintf( 183 | 'File %s does not exist.', $file 184 | )); 185 | } 186 | 187 | $__file = $file; 188 | 189 | $template = function($__vars) use ($__file) { 190 | extract($__vars); 191 | unset($__vars); 192 | 193 | ob_start(); 194 | include($__file); 195 | return ob_get_clean(); 196 | }; 197 | 198 | return $template; 199 | } 200 | 201 | # Public: Runs a system command. Let's the build fail when the command did exit with a 202 | # status indicating an error. 203 | # 204 | # cmd - Command with arguments as String or List. Lists get joined by a single space. 205 | # callback - A callback which receives the success as Boolean 206 | # and the Process instance as second argument (optional). 207 | # options - Array of options: 208 | # timeout - Timeout in seconds. 209 | # failOnError - Should the build fail when the command failed with a non-zero 210 | # exit status? (default: false) 211 | # 212 | # Examples 213 | # 214 | # # The command's output is displayed on STDOUT and the build fails when the exit code 215 | # # was greater than zero. 216 | # sh('ls -l'); 217 | # 218 | # # When a callback is passed as second argument, then the callback 219 | # # receives the success status ($ok) as Boolean and a process instance 220 | # # as second argument. 221 | # sh('ls -A', function($ok, $process) { 222 | # $ok or fwrite($process->getErrorOutput(), STDERR); 223 | # }); 224 | # 225 | # Returns nothing. 226 | function sh($cmd, $callback = null, $options = array()) 227 | { 228 | $cmd = join(' ', (array) $cmd); 229 | $showCmd = strlen($cmd) > 42 ? "..." . substr($cmd, -42) : $cmd; 230 | 231 | logger()->info("sh($showCmd)"); 232 | 233 | if (func_num_args() == 2 and is_array($callback) and !is_callable($callback)) { 234 | $options = $callback; 235 | $callback = null; 236 | } 237 | 238 | $timeout = @$options["timeout"]; 239 | $failOnError = @$options["failOnError"] ?: @$options['fail_on_error']; 240 | 241 | $process = new Process($cmd); 242 | $process->setTimeout($timeout); 243 | 244 | $process->run(function($type, $output) { 245 | $type == 'err' ? fwrite(STDERR, $output) : print($output); 246 | }); 247 | 248 | if (!$process->isSuccessful() and $failOnError) { 249 | failf("Command failed with status (%d) [%s]", array($process->getExitCode(), $showCmd)); 250 | } 251 | 252 | if ($callback !== null) { 253 | call_user_func($callback, $process->isSuccessful(), $process); 254 | } 255 | } 256 | 257 | # Public: Run a PHP Process with the given arguments. 258 | # 259 | # argv - The argv either as Array or String. Arrays get joined by a single space. 260 | # callback - See sh(). 261 | # options - See sh(). 262 | # 263 | # Examples 264 | # 265 | # # Runs a PHP dev server on `localhost:4000` with the document root 266 | # # `public/` and the router script `public/index.php` (requires PHP >= 5.4). 267 | # php(array('-S', 'localhost:4000', '-t', 'public/', 'public/index.php')); 268 | # 269 | # Returns nothing. 270 | function php($argv, $callback = null, $options = array()) 271 | { 272 | $execFinder = new \Symfony\Component\Process\PhpExecutableFinder; 273 | $php = $execFinder->find(); 274 | 275 | $argv = (array) $argv; 276 | array_unshift($argv, $php); 277 | 278 | return sh($argv, $callback, $options); 279 | } 280 | 281 | # Public: Creates a new finder from the list of patterns. 282 | # 283 | # patterns - List of shell file patterns. 284 | # 285 | # Returns a new Finder. 286 | function fileList($patterns) 287 | { 288 | $patterns = (array) $patterns; 289 | 290 | $finder = new \Symfony\Component\Finder\Finder; 291 | $finder->files(); 292 | 293 | foreach ($patterns as $p) { 294 | $finder->name($p); 295 | } 296 | 297 | return $finder; 298 | } 299 | 300 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bob, your friendly builder 2 | ========================== 3 | 4 | [![Build Status](https://travis-ci.org/CHH/bob.png?branch=master)](https://travis-ci.org/CHH/bob) 5 | 6 | - - - 7 | ## Hello World 8 | 9 | Put this in a file named `bob_config.php` in your project's root: 10 | 11 | ```php 12 | `. 44 | - Bob **has no** file finder similar to `pakeFinder`, if you need this 45 | just use the [Symfony Finder](https://github.com/symfony/Finder). 46 | 47 | **How Bob compares to [Phing](http://www.phing.info/trac/):** 48 | 49 | - Bob **does not use XML config files** to define tasks. I think build 50 | files should be written in the language used to write the project 51 | itself so the barrier to contribution to build files is as low as possible. 52 | Also I think it's quite hilarious to use XML for a DSL with logic and such. 53 | - Bob has nothing like plugins. To add new functions to Bob's DSL just 54 | put them into the `Bob\BuildConfig` namespace and require the file somehow at the 55 | beginning of your build file. **Simply put:** Bob's build configs are _only_ PHP. 56 | - Bob has **no** rich set of provided tasks and I do not plan to add 57 | this. _Bob is lightweight._ 58 | 59 | Getting Started 60 | --------------- 61 | 62 | ### Install 63 | 64 | #### Prerequisites 65 | 66 | Bob needs at least **PHP 5.3.2** to run. 67 | 68 | If you plan to hack on Bob, please make sure you 69 | have set `phar.readonly` to `Off` in your `php.ini`. Otherwise you will have a tough luck 70 | creating a PHAR package of Bob. 71 | 72 | #### Install into a [Composer](https://github.com/composer/composer)-enabled Project (recommended) 73 | 74 | Add the `chh/bob` package to the `require-dev` section in your 75 | `composer.json`: 76 | 77 | { 78 | "require-dev": { 79 | "chh/bob": "1.0.*@dev" 80 | } 81 | } 82 | 83 | Then run `composer install --dev`. 84 | 85 | You can invoke Bob with: 86 | 87 | php vendor/bin/bob 88 | 89 | or: 90 | 91 | ./vendor/bin/bob 92 | 93 | #### System-wide install (Unix-like OS only) 94 | 95 | To do a system-wide install, download either a [Release][tags] or 96 | clone the Repository with: 97 | 98 | $ git clone git://github.com/CHH/bob.git 99 | $ cd Bob 100 | 101 | [tags]: https://github.com/CHH/bob/tags 102 | 103 | To install all of Bob's dependencies download Composer and run 104 | `composer install`: 105 | 106 | $ wget http://getcomposer.org/composer.phar 107 | php composer.phar install 108 | 109 | Then run `php bin/bob install` and you're done. 110 | 111 | By default the `bob` command is created in `/usr/local/bin`. To change 112 | this set a `PREFIX` environment variable, the command is then created 113 | in `$PREFIX/bin`. 114 | 115 | ### Prepare your project 116 | 117 | You can output a usage message by running 118 | 119 | $ php vendor/bin/bob --help 120 | 121 | First run in your projects root directory Bob with the `--init` flag. 122 | This creates an empty `bob_config.php` with one example task: 123 | 124 | $ php vendor/bin/bob --init 125 | 126 | Bob loads your tasks from a special file named `bob_config.php` in your project's 127 | root directory. Bob also includes all files found in a directory 128 | named `bob_tasks`, in the same directory as your `bob_config.php`. Files loaded from 129 | `bob_tasks` are treated the same way as regular Bob configs. 130 | 131 | It's important that you declare that this file belongs to the 132 | `Bob\BuildConfig` namespace with `namespace Bob\BuildConfig;`, otherwise the DSL functions are 133 | not available. 134 | 135 | --- 136 | 137 | **Hint:** It doesn't matter if you're in a sub-directory of your 138 | project, Bob _will_ find your `bob_config.php` by wandering up 139 | the directory tree. 140 | 141 | --- 142 | 143 | Now let's define our first task. This task will output "Hello World": 144 | 145 | task('hello', function() { 146 | println('Hello World'); 147 | }); 148 | 149 | Tasks are run by using their name as argument(s) on the command line: 150 | 151 | $ php vendor/bin/bob hello 152 | 153 | When Bob is invoked without tasks it tries to invoke 154 | the `default` task. 155 | 156 | To set a task as default task assign the task as prerequisite 157 | of the `default` task: 158 | 159 | task('default', array('hello')); 160 | 161 | You know, tasks should be self-documenting, you really don't want a 162 | manual for your build config or do you? Bob provides the 163 | `desc` function for that. Let's add some text to our task, which describes 164 | what the task is all about: 165 | 166 | desc('Prints Hello World to the Command Line'); 167 | task('hello', function() { 168 | println('Hello World'); 169 | }); 170 | 171 | To view all tasks and their descriptions pass the `--tasks` flag: 172 | 173 | $ php vendor/bin/bob --tasks 174 | bob hello # Prints Hello World to the Command Line 175 | 176 | --- 177 | 178 | To see more examples for how Bob can be used, simply look into Bob's 179 | `bob_config.php`. It contains all configuration to create Bob's build 180 | artifacts, for example the `bob.phar` and the composer config. 181 | 182 | ### File Tasks 183 | 184 | A file task is a special kind of task, which gets only run if either the 185 | target (the product of some operation) does not exist, or the 186 | prerequisites are newer than the target. 187 | 188 | So file tasks are very handy if you've some artifacts which are 189 | generated from other files, and which you don't want to regenerate 190 | if nothing has changed. 191 | 192 | For example: Let's write a task which concatenates three input files to 193 | one output file. 194 | 195 | First we have to create the prerequisites: 196 | 197 | $ echo "foo\n" > file1.txt 198 | $ echo "bar\n" > file2.txt 199 | $ echo "baz\n" > file3.txt 200 | 201 | Then put this into your `bob_config.php`: 202 | 203 | fileTask('concat.txt', array('file1.txt', 'file2.txt', 'file3.txt'), function($task) { 204 | println("Concatenating"); 205 | $concat = ''; 206 | 207 | foreach ($task->prerequisites as $file) { 208 | $concat .= file_get_contents($file); 209 | } 210 | 211 | @file_put_contents($task->name, $concat); 212 | }); 213 | 214 | Let's run this task: 215 | 216 | $ php vendor/bin/bob concat.txt 217 | Concatenating 218 | 219 | This will result in a `concat.txt` file in your project root: 220 | 221 | $ cat concat.txt 222 | foo 223 | bar 224 | baz 225 | 226 | Let's run it again, without modifying the prerequisites: 227 | 228 | $ php vendor/bin/bob concat.txt 229 | 230 | See it? The callback was not run, because the prerequisites were not modified. 231 | 232 | Let's verify this: 233 | 234 | $ touch file1.txt 235 | $ php vendor/bin/bob concat.txt 236 | Concatenating 237 | 238 | The prerequisites of a file task are also resolved as task names, so 239 | they can depend on other file tasks too. Or you can put regular task 240 | names into the prerequisites, but then you've to be careful to not 241 | accidentally treat them as files when looping through all prerequisites. 242 | 243 | ### Packaging tasks for reusability 244 | 245 | Ever did write a collection of tasks which you want to put into a 246 | package and reuse across projects? 247 | 248 | Enter Task Libraries. 249 | 250 | All task libraries implement the `\Bob\TaskLibraryInterface` which 251 | defines two methods: 252 | 253 | * `register(\Bob\Application $app)`: Is called when the library is 254 | registered in the app. 255 | * `boot(\Bob\Application $app)`: is called just before the Bob is run on 256 | the command line. Register your tasks here. 257 | 258 | Here's a small example which registers a `test` task which uses PHPUnit: 259 | 260 | ```php 261 | fileTask("phpunit.xml", array("phpunit.dist.xml"), function($task) { 275 | copy($task->prerequisites->current(), $task->name); 276 | }); 277 | 278 | $app->task("test", array("phpunit.xml"), function($task) { 279 | b\sh("./vendor/bin/phpunit"); 280 | 281 | })->description = "Runs the test suite"; 282 | } 283 | } 284 | ``` 285 | 286 | You can use task libraries by calling the `register` function within 287 | your build scripts: 288 | 289 | ```php 290 | =5.3.2" 24 | }, 25 | "type": "library", 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "1.0.x-dev" 29 | } 30 | }, 31 | "autoload": { 32 | "psr-0": { 33 | "CHH\\FileUtils": "lib/" 34 | } 35 | }, 36 | "notification-url": "https://packagist.org/downloads/", 37 | "license": [ 38 | "MIT" 39 | ], 40 | "authors": [ 41 | { 42 | "name": "Christoph Hochstrasser", 43 | "email": "christoph.hochstrasser@gmail.com", 44 | "homepage": "http://christophh.net" 45 | } 46 | ], 47 | "description": "Some utility functions for handling files", 48 | "homepage": "https://github.com/CHH/fileutils", 49 | "keywords": [ 50 | "file", 51 | "util" 52 | ], 53 | "time": "2013-01-07 17:31:32" 54 | }, 55 | { 56 | "name": "chh/itertools", 57 | "version": "v1.0.0", 58 | "source": { 59 | "type": "git", 60 | "url": "http://github.com/CHH/itertools", 61 | "reference": "v1.0.0" 62 | }, 63 | "dist": { 64 | "type": "zip", 65 | "url": "https://github.com/CHH/itertools/archive/v1.0.0.zip", 66 | "reference": "v1.0.0", 67 | "shasum": "" 68 | }, 69 | "require": { 70 | "ext-spl": "*", 71 | "php": ">=5.3.3" 72 | }, 73 | "require-dev": { 74 | "chh/bob": "*@dev", 75 | "phpunit/phpunit": ">=3.7,<4.0" 76 | }, 77 | "type": "library", 78 | "extra": { 79 | "branch-alias": { 80 | "dev-master": "1.0.x-dev" 81 | } 82 | }, 83 | "autoload": { 84 | "psr-0": { 85 | "itertools\\": "lib/" 86 | }, 87 | "files": [ 88 | "lib/itertools.php" 89 | ] 90 | }, 91 | "notification-url": "https://packagist.org/downloads/", 92 | "license": [ 93 | "MIT" 94 | ], 95 | "authors": [ 96 | { 97 | "name": "Christoph Hochstrasser", 98 | "email": "christoph.hochstrasser@gmail.com", 99 | "homepage": "http://christophh.net" 100 | } 101 | ], 102 | "description": "Common functional operations for iterators.", 103 | "keywords": [ 104 | "functional", 105 | "iterators" 106 | ], 107 | "time": "2013-01-08 17:10:36" 108 | }, 109 | { 110 | "name": "chh/optparse", 111 | "version": "v0.1.0", 112 | "source": { 113 | "type": "git", 114 | "url": "git://github.com/CHH/optparse", 115 | "reference": "v0.1.0" 116 | }, 117 | "dist": { 118 | "type": "zip", 119 | "url": "https://github.com/CHH/optparse/archive/v0.1.0.zip", 120 | "reference": "v0.1.0", 121 | "shasum": "" 122 | }, 123 | "require": { 124 | "php": ">=5.3.3" 125 | }, 126 | "type": "library", 127 | "autoload": { 128 | "classmap": [ 129 | "lib/" 130 | ] 131 | }, 132 | "notification-url": "https://packagist.org/downloads/", 133 | "license": [ 134 | "mit" 135 | ], 136 | "authors": [ 137 | { 138 | "name": "Christoph Hochstrasser", 139 | "email": "christoph.hochstrasser@gmail.com", 140 | "homepage": "http://christophh.net" 141 | } 142 | ], 143 | "description": "Easy to use option parser.", 144 | "keywords": [ 145 | "cli" 146 | ], 147 | "time": "2012-12-19 18:13:05" 148 | }, 149 | { 150 | "name": "monolog/monolog", 151 | "version": "1.7.0", 152 | "source": { 153 | "type": "git", 154 | "url": "https://github.com/Seldaek/monolog.git", 155 | "reference": "6225b22de9dcf36546be3a0b2fa8e3d986153f57" 156 | }, 157 | "dist": { 158 | "type": "zip", 159 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/6225b22de9dcf36546be3a0b2fa8e3d986153f57", 160 | "reference": "6225b22de9dcf36546be3a0b2fa8e3d986153f57", 161 | "shasum": "" 162 | }, 163 | "require": { 164 | "php": ">=5.3.0", 165 | "psr/log": "~1.0" 166 | }, 167 | "require-dev": { 168 | "aws/aws-sdk-php": "~2.4.8", 169 | "doctrine/couchdb": "dev-master", 170 | "mlehner/gelf-php": "1.0.*", 171 | "phpunit/phpunit": "~3.7.0", 172 | "raven/raven": "0.5.*", 173 | "ruflin/elastica": "0.90.*" 174 | }, 175 | "suggest": { 176 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 177 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 178 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 179 | "ext-mongo": "Allow sending log messages to a MongoDB server", 180 | "mlehner/gelf-php": "Allow sending log messages to a GrayLog2 server", 181 | "raven/raven": "Allow sending log messages to a Sentry server", 182 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server" 183 | }, 184 | "type": "library", 185 | "extra": { 186 | "branch-alias": { 187 | "dev-master": "1.7.x-dev" 188 | } 189 | }, 190 | "autoload": { 191 | "psr-0": { 192 | "Monolog": "src/" 193 | } 194 | }, 195 | "notification-url": "https://packagist.org/downloads/", 196 | "license": [ 197 | "MIT" 198 | ], 199 | "authors": [ 200 | { 201 | "name": "Jordi Boggiano", 202 | "email": "j.boggiano@seld.be", 203 | "homepage": "http://seld.be", 204 | "role": "Developer" 205 | } 206 | ], 207 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 208 | "homepage": "http://github.com/Seldaek/monolog", 209 | "keywords": [ 210 | "log", 211 | "logging", 212 | "psr-3" 213 | ], 214 | "time": "2013-11-14 19:48:31" 215 | }, 216 | { 217 | "name": "pimple/pimple", 218 | "version": "v1.1.0", 219 | "source": { 220 | "type": "git", 221 | "url": "https://github.com/fabpot/Pimple.git", 222 | "reference": "471c7d7c52ad6594e17b8ec33efdd1be592b5d83" 223 | }, 224 | "dist": { 225 | "type": "zip", 226 | "url": "https://api.github.com/repos/fabpot/Pimple/zipball/471c7d7c52ad6594e17b8ec33efdd1be592b5d83", 227 | "reference": "471c7d7c52ad6594e17b8ec33efdd1be592b5d83", 228 | "shasum": "" 229 | }, 230 | "require": { 231 | "php": ">=5.3.0" 232 | }, 233 | "type": "library", 234 | "extra": { 235 | "branch-alias": { 236 | "dev-master": "1.1.x-dev" 237 | } 238 | }, 239 | "autoload": { 240 | "psr-0": { 241 | "Pimple": "lib/" 242 | } 243 | }, 244 | "notification-url": "https://packagist.org/downloads/", 245 | "license": [ 246 | "MIT" 247 | ], 248 | "authors": [ 249 | { 250 | "name": "Fabien Potencier", 251 | "email": "fabien@symfony.com" 252 | } 253 | ], 254 | "description": "Pimple is a simple Dependency Injection Container for PHP 5.3", 255 | "homepage": "http://pimple.sensiolabs.org", 256 | "keywords": [ 257 | "container", 258 | "dependency injection" 259 | ], 260 | "time": "2013-09-19 04:53:08" 261 | }, 262 | { 263 | "name": "psr/log", 264 | "version": "1.0.0", 265 | "source": { 266 | "type": "git", 267 | "url": "https://github.com/php-fig/log", 268 | "reference": "1.0.0" 269 | }, 270 | "dist": { 271 | "type": "zip", 272 | "url": "https://github.com/php-fig/log/archive/1.0.0.zip", 273 | "reference": "1.0.0", 274 | "shasum": "" 275 | }, 276 | "type": "library", 277 | "autoload": { 278 | "psr-0": { 279 | "Psr\\Log\\": "" 280 | } 281 | }, 282 | "notification-url": "https://packagist.org/downloads/", 283 | "license": [ 284 | "MIT" 285 | ], 286 | "authors": [ 287 | { 288 | "name": "PHP-FIG", 289 | "homepage": "http://www.php-fig.org/" 290 | } 291 | ], 292 | "description": "Common interface for logging libraries", 293 | "keywords": [ 294 | "log", 295 | "psr", 296 | "psr-3" 297 | ], 298 | "time": "2012-12-21 11:40:51" 299 | }, 300 | { 301 | "name": "symfony/finder", 302 | "version": "v2.3.7", 303 | "target-dir": "Symfony/Component/Finder", 304 | "source": { 305 | "type": "git", 306 | "url": "https://github.com/symfony/Finder.git", 307 | "reference": "a175521f680b178e63c5d0ab87c6b046c0990c3f" 308 | }, 309 | "dist": { 310 | "type": "zip", 311 | "url": "https://api.github.com/repos/symfony/Finder/zipball/a175521f680b178e63c5d0ab87c6b046c0990c3f", 312 | "reference": "a175521f680b178e63c5d0ab87c6b046c0990c3f", 313 | "shasum": "" 314 | }, 315 | "require": { 316 | "php": ">=5.3.3" 317 | }, 318 | "type": "library", 319 | "extra": { 320 | "branch-alias": { 321 | "dev-master": "2.3-dev" 322 | } 323 | }, 324 | "autoload": { 325 | "psr-0": { 326 | "Symfony\\Component\\Finder\\": "" 327 | } 328 | }, 329 | "notification-url": "https://packagist.org/downloads/", 330 | "license": [ 331 | "MIT" 332 | ], 333 | "authors": [ 334 | { 335 | "name": "Fabien Potencier", 336 | "email": "fabien@symfony.com" 337 | }, 338 | { 339 | "name": "Symfony Community", 340 | "homepage": "http://symfony.com/contributors" 341 | } 342 | ], 343 | "description": "Symfony Finder Component", 344 | "homepage": "http://symfony.com", 345 | "time": "2013-09-19 09:45:20" 346 | }, 347 | { 348 | "name": "symfony/process", 349 | "version": "v2.3.7", 350 | "target-dir": "Symfony/Component/Process", 351 | "source": { 352 | "type": "git", 353 | "url": "https://github.com/symfony/Process.git", 354 | "reference": "82898108f79040314a7b3ba430a72c32c7f61d14" 355 | }, 356 | "dist": { 357 | "type": "zip", 358 | "url": "https://api.github.com/repos/symfony/Process/zipball/82898108f79040314a7b3ba430a72c32c7f61d14", 359 | "reference": "82898108f79040314a7b3ba430a72c32c7f61d14", 360 | "shasum": "" 361 | }, 362 | "require": { 363 | "php": ">=5.3.3" 364 | }, 365 | "type": "library", 366 | "extra": { 367 | "branch-alias": { 368 | "dev-master": "2.3-dev" 369 | } 370 | }, 371 | "autoload": { 372 | "psr-0": { 373 | "Symfony\\Component\\Process\\": "" 374 | } 375 | }, 376 | "notification-url": "https://packagist.org/downloads/", 377 | "license": [ 378 | "MIT" 379 | ], 380 | "authors": [ 381 | { 382 | "name": "Fabien Potencier", 383 | "email": "fabien@symfony.com" 384 | }, 385 | { 386 | "name": "Symfony Community", 387 | "homepage": "http://symfony.com/contributors" 388 | } 389 | ], 390 | "description": "Symfony Process Component", 391 | "homepage": "http://symfony.com", 392 | "time": "2013-10-30 08:30:20" 393 | } 394 | ], 395 | "packages-dev": [ 396 | { 397 | "name": "phpunit/php-code-coverage", 398 | "version": "1.2.13", 399 | "source": { 400 | "type": "git", 401 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 402 | "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94" 403 | }, 404 | "dist": { 405 | "type": "zip", 406 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", 407 | "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", 408 | "shasum": "" 409 | }, 410 | "require": { 411 | "php": ">=5.3.3", 412 | "phpunit/php-file-iterator": ">=1.3.0@stable", 413 | "phpunit/php-text-template": ">=1.1.1@stable", 414 | "phpunit/php-token-stream": ">=1.1.3@stable" 415 | }, 416 | "require-dev": { 417 | "phpunit/phpunit": "3.7.*@dev" 418 | }, 419 | "suggest": { 420 | "ext-dom": "*", 421 | "ext-xdebug": ">=2.0.5" 422 | }, 423 | "type": "library", 424 | "extra": { 425 | "branch-alias": { 426 | "dev-master": "1.2.x-dev" 427 | } 428 | }, 429 | "autoload": { 430 | "classmap": [ 431 | "PHP/" 432 | ] 433 | }, 434 | "notification-url": "https://packagist.org/downloads/", 435 | "include-path": [ 436 | "" 437 | ], 438 | "license": [ 439 | "BSD-3-Clause" 440 | ], 441 | "authors": [ 442 | { 443 | "name": "Sebastian Bergmann", 444 | "email": "sb@sebastian-bergmann.de", 445 | "role": "lead" 446 | } 447 | ], 448 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 449 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 450 | "keywords": [ 451 | "coverage", 452 | "testing", 453 | "xunit" 454 | ], 455 | "time": "2013-09-10 08:14:32" 456 | }, 457 | { 458 | "name": "phpunit/php-file-iterator", 459 | "version": "1.3.4", 460 | "source": { 461 | "type": "git", 462 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 463 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" 464 | }, 465 | "dist": { 466 | "type": "zip", 467 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", 468 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", 469 | "shasum": "" 470 | }, 471 | "require": { 472 | "php": ">=5.3.3" 473 | }, 474 | "type": "library", 475 | "autoload": { 476 | "classmap": [ 477 | "File/" 478 | ] 479 | }, 480 | "notification-url": "https://packagist.org/downloads/", 481 | "include-path": [ 482 | "" 483 | ], 484 | "license": [ 485 | "BSD-3-Clause" 486 | ], 487 | "authors": [ 488 | { 489 | "name": "Sebastian Bergmann", 490 | "email": "sb@sebastian-bergmann.de", 491 | "role": "lead" 492 | } 493 | ], 494 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 495 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 496 | "keywords": [ 497 | "filesystem", 498 | "iterator" 499 | ], 500 | "time": "2013-10-10 15:34:57" 501 | }, 502 | { 503 | "name": "phpunit/php-text-template", 504 | "version": "1.1.4", 505 | "source": { 506 | "type": "git", 507 | "url": "git://github.com/sebastianbergmann/php-text-template.git", 508 | "reference": "1.1.4" 509 | }, 510 | "dist": { 511 | "type": "zip", 512 | "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.4", 513 | "reference": "1.1.4", 514 | "shasum": "" 515 | }, 516 | "require": { 517 | "php": ">=5.3.3" 518 | }, 519 | "type": "library", 520 | "autoload": { 521 | "classmap": [ 522 | "Text/" 523 | ] 524 | }, 525 | "notification-url": "https://packagist.org/downloads/", 526 | "include-path": [ 527 | "" 528 | ], 529 | "license": [ 530 | "BSD-3-Clause" 531 | ], 532 | "authors": [ 533 | { 534 | "name": "Sebastian Bergmann", 535 | "email": "sb@sebastian-bergmann.de", 536 | "role": "lead" 537 | } 538 | ], 539 | "description": "Simple template engine.", 540 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 541 | "keywords": [ 542 | "template" 543 | ], 544 | "time": "2012-10-31 11:15:28" 545 | }, 546 | { 547 | "name": "phpunit/php-timer", 548 | "version": "1.0.5", 549 | "source": { 550 | "type": "git", 551 | "url": "https://github.com/sebastianbergmann/php-timer.git", 552 | "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" 553 | }, 554 | "dist": { 555 | "type": "zip", 556 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", 557 | "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", 558 | "shasum": "" 559 | }, 560 | "require": { 561 | "php": ">=5.3.3" 562 | }, 563 | "type": "library", 564 | "autoload": { 565 | "classmap": [ 566 | "PHP/" 567 | ] 568 | }, 569 | "notification-url": "https://packagist.org/downloads/", 570 | "include-path": [ 571 | "" 572 | ], 573 | "license": [ 574 | "BSD-3-Clause" 575 | ], 576 | "authors": [ 577 | { 578 | "name": "Sebastian Bergmann", 579 | "email": "sb@sebastian-bergmann.de", 580 | "role": "lead" 581 | } 582 | ], 583 | "description": "Utility class for timing", 584 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 585 | "keywords": [ 586 | "timer" 587 | ], 588 | "time": "2013-08-02 07:42:54" 589 | }, 590 | { 591 | "name": "phpunit/php-token-stream", 592 | "version": "1.2.1", 593 | "source": { 594 | "type": "git", 595 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 596 | "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e" 597 | }, 598 | "dist": { 599 | "type": "zip", 600 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", 601 | "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", 602 | "shasum": "" 603 | }, 604 | "require": { 605 | "ext-tokenizer": "*", 606 | "php": ">=5.3.3" 607 | }, 608 | "type": "library", 609 | "extra": { 610 | "branch-alias": { 611 | "dev-master": "1.2-dev" 612 | } 613 | }, 614 | "autoload": { 615 | "classmap": [ 616 | "PHP/" 617 | ] 618 | }, 619 | "notification-url": "https://packagist.org/downloads/", 620 | "include-path": [ 621 | "" 622 | ], 623 | "license": [ 624 | "BSD-3-Clause" 625 | ], 626 | "authors": [ 627 | { 628 | "name": "Sebastian Bergmann", 629 | "email": "sb@sebastian-bergmann.de", 630 | "role": "lead" 631 | } 632 | ], 633 | "description": "Wrapper around PHP's tokenizer extension.", 634 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 635 | "keywords": [ 636 | "tokenizer" 637 | ], 638 | "time": "2013-09-13 04:58:23" 639 | }, 640 | { 641 | "name": "phpunit/phpunit", 642 | "version": "3.7.28", 643 | "source": { 644 | "type": "git", 645 | "url": "https://github.com/sebastianbergmann/phpunit.git", 646 | "reference": "3b97c8492bcafbabe6b6fbd2ab35f2f04d932a8d" 647 | }, 648 | "dist": { 649 | "type": "zip", 650 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3b97c8492bcafbabe6b6fbd2ab35f2f04d932a8d", 651 | "reference": "3b97c8492bcafbabe6b6fbd2ab35f2f04d932a8d", 652 | "shasum": "" 653 | }, 654 | "require": { 655 | "ext-dom": "*", 656 | "ext-pcre": "*", 657 | "ext-reflection": "*", 658 | "ext-spl": "*", 659 | "php": ">=5.3.3", 660 | "phpunit/php-code-coverage": "~1.2.1", 661 | "phpunit/php-file-iterator": ">=1.3.1", 662 | "phpunit/php-text-template": ">=1.1.1", 663 | "phpunit/php-timer": ">=1.0.4", 664 | "phpunit/phpunit-mock-objects": "~1.2.0", 665 | "symfony/yaml": "~2.0" 666 | }, 667 | "require-dev": { 668 | "pear-pear/pear": "1.9.4" 669 | }, 670 | "suggest": { 671 | "ext-json": "*", 672 | "ext-simplexml": "*", 673 | "ext-tokenizer": "*", 674 | "phpunit/php-invoker": ">=1.1.0,<1.2.0" 675 | }, 676 | "bin": [ 677 | "composer/bin/phpunit" 678 | ], 679 | "type": "library", 680 | "extra": { 681 | "branch-alias": { 682 | "dev-master": "3.7.x-dev" 683 | } 684 | }, 685 | "autoload": { 686 | "classmap": [ 687 | "PHPUnit/" 688 | ] 689 | }, 690 | "notification-url": "https://packagist.org/downloads/", 691 | "include-path": [ 692 | "", 693 | "../../symfony/yaml/" 694 | ], 695 | "license": [ 696 | "BSD-3-Clause" 697 | ], 698 | "authors": [ 699 | { 700 | "name": "Sebastian Bergmann", 701 | "email": "sebastian@phpunit.de", 702 | "role": "lead" 703 | } 704 | ], 705 | "description": "The PHP Unit Testing framework.", 706 | "homepage": "http://www.phpunit.de/", 707 | "keywords": [ 708 | "phpunit", 709 | "testing", 710 | "xunit" 711 | ], 712 | "time": "2013-10-17 07:27:40" 713 | }, 714 | { 715 | "name": "phpunit/phpunit-mock-objects", 716 | "version": "1.2.3", 717 | "source": { 718 | "type": "git", 719 | "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", 720 | "reference": "1.2.3" 721 | }, 722 | "dist": { 723 | "type": "zip", 724 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", 725 | "reference": "1.2.3", 726 | "shasum": "" 727 | }, 728 | "require": { 729 | "php": ">=5.3.3", 730 | "phpunit/php-text-template": ">=1.1.1@stable" 731 | }, 732 | "suggest": { 733 | "ext-soap": "*" 734 | }, 735 | "type": "library", 736 | "autoload": { 737 | "classmap": [ 738 | "PHPUnit/" 739 | ] 740 | }, 741 | "notification-url": "https://packagist.org/downloads/", 742 | "include-path": [ 743 | "" 744 | ], 745 | "license": [ 746 | "BSD-3-Clause" 747 | ], 748 | "authors": [ 749 | { 750 | "name": "Sebastian Bergmann", 751 | "email": "sb@sebastian-bergmann.de", 752 | "role": "lead" 753 | } 754 | ], 755 | "description": "Mock Object library for PHPUnit", 756 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 757 | "keywords": [ 758 | "mock", 759 | "xunit" 760 | ], 761 | "time": "2013-01-13 10:24:48" 762 | }, 763 | { 764 | "name": "symfony/yaml", 765 | "version": "v2.3.7", 766 | "target-dir": "Symfony/Component/Yaml", 767 | "source": { 768 | "type": "git", 769 | "url": "https://github.com/symfony/Yaml.git", 770 | "reference": "c1bda5b459d792cb253de12c65beba3040163b2b" 771 | }, 772 | "dist": { 773 | "type": "zip", 774 | "url": "https://api.github.com/repos/symfony/Yaml/zipball/c1bda5b459d792cb253de12c65beba3040163b2b", 775 | "reference": "c1bda5b459d792cb253de12c65beba3040163b2b", 776 | "shasum": "" 777 | }, 778 | "require": { 779 | "php": ">=5.3.3" 780 | }, 781 | "type": "library", 782 | "extra": { 783 | "branch-alias": { 784 | "dev-master": "2.3-dev" 785 | } 786 | }, 787 | "autoload": { 788 | "psr-0": { 789 | "Symfony\\Component\\Yaml\\": "" 790 | } 791 | }, 792 | "notification-url": "https://packagist.org/downloads/", 793 | "license": [ 794 | "MIT" 795 | ], 796 | "authors": [ 797 | { 798 | "name": "Fabien Potencier", 799 | "email": "fabien@symfony.com" 800 | }, 801 | { 802 | "name": "Symfony Community", 803 | "homepage": "http://symfony.com/contributors" 804 | } 805 | ], 806 | "description": "Symfony Yaml Component", 807 | "homepage": "http://symfony.com", 808 | "time": "2013-10-17 11:48:01" 809 | } 810 | ], 811 | "aliases": [ 812 | 813 | ], 814 | "minimum-stability": "stable", 815 | "stability-flags": [ 816 | 817 | ], 818 | "platform": { 819 | "php": ">=5.3.3" 820 | }, 821 | "platform-dev": [ 822 | 823 | ] 824 | } 825 | --------------------------------------------------------------------------------