# Extending Cartage Cartage is designed for extension. There are two distinct *types* of extension that can be written: *commands* and *plug-ins*. Most *command* extensions will implement a *plug-in* extension, but Cartage can be extended with either mechanism on its own. For both *commands* and *plug-ins* there is automatic discovery and activation. ## Command Extensions Command extensions add new commands and subcommands to the Cartage CLI (`bin/cartage`). The Cartage CLI is written with the [GLI][] DSL, so all of the features present in GLI can be used to add commands to Cartage itself. Commands are discovered by placing a command definition file in `lib/cartage/commands` in the gem containing the command extension. ### Defining a Command New commands *extend* Cartage::CLI, opening a context where the GLI DSL can be used to create a new command. Outside of the new command block, it is recommended that only `desc`, `long_desc`, `arg`, and `command` be used since there is no way of adding support for new global switches or flags. ```ruby Cartage::CLI.extend do # Define the commands here. desc 'A new command' long_desc 'A long description for the new command' arg :maybe, :optional arg :yes # required by default arg :items, :multiple command 'newcommand' do |echo| echo.desc 'Enable fuzz mode.' echo.switch :fuzz echo.desc 'Fuzz level.' echo.flag 'fuzz-level', arg_name: :LEVEL, default_value: 0 echo.action do |global, options, args| # Whatever the new command does should be done here. end end end ``` It is recommended that a command extension add only one new command with subcommands to handle more complex operations. ```ruby Cartage::CLI.extend do command 'newcommand' do |echo| echo.command 'subcommand' do |sc| sc.action do |global, options, args| # subcommand actions end end echo.default_command :subcommand end end ``` ### Global Cartage Instance Each defined command operates in the same global context and has access to a method (`cartage`) that returns the operating instance of Cartage for the execution of the `cartage` command. ```ruby Cartage::CLI.extend do desc 'Echo the provided text' arg :TEXT, :multiple command 'echo' do |echo| echo.desc 'Suppress newlines' echo.switch [ :c, 'newlines' ], default: true, negatable: false echo.action do |_g, options, args| unless cartage.quiet message = args.join(' ') if options['no-newlines'] || cartage.config(for_command: 'echo').no_newlines puts message else print message end end end end end ``` In the example above, the `echo` command checks the global configuration for the value of the `quiet` switch, and the command options and configuration for whether newlines should be included in the output. Note the use of Cartage#config with the `for_command` keyword parameter. ### Exiting the Command If the command is successful, completing its action block without raising an exception will be sufficient. If an exception is thrown, it will be caught, its message displayed, and `cartage` will exit with a non-zero exit status. Cartage::CLI provides three *special* exceptions: Cartage::CLI::QuietExit : This will abort or exit the command with the provided exit status. No message will be displayed. This is used in `cartage manifest check`. Cartage::CLI::CustomExit : An alias for GLI::CustomExit. Raised with a message and an exit status. The message will be displayed, and the exit status will be returned. This should be used for *failure* of the command. Cartage::CLI::CommandException : An alias for GLI::CommandException. This exception is similar to CustomExit, but should be used when there is a configuration issue, or a conflict in flags or switches. It requires three parameters: the message to display, the command name for help purposes, and the exit status. ### Example (`info` Command) Below is an example command and sub-commands, `cartage info`. This has limited utility, but will illustrate how commands are created in Cartage. This example is usable, as the `info` command is a hidden command on `cartage`. ```ruby # Call Cartage::CLI.extend to add the commands defined in the block. Cartage::CLI.extend do # Use +desc+ to provide a description about the next command. desc 'Show information about Cartage itself' # Use +long_desc+ to provide a long description about the next command that # can # be included in a generated RDoc file. long_desc <<-'DESC' Shows various bits of information about Cartage based on the running instance. Some plug-ins do run-time resolution of configuration values, so what is shown through these subcommands will not represent that resolution. DESC # Declare a new command with +command+ and one or more names for the command. # This command is available as both cartage info and cartage # i. The block contains the definition of the switches, flags, # subcommands, and actions for this command. command %w(info i) do |info| # The same GLI::DSL works on the command being created, Here, we are # creating cartage info plugins. info.desc 'Show the plug-ins that have been found.' info.long_desc <<-'DESC' Only plug-ins using the class plug-in protocol are found this way. Plug-ins that just provide commands are not reported as plugins. DESC info.command 'plugins' do |plugins| # Implement the #action as a block. It accepts three parameters: the # global options hash, the local command options hash, and any arguments # passed on the command-line. # # Within a command's action, a +cartage+ object is available that # represents the Cartage instance that will be used for packaging. plugins.action do |_global, _options, _args| plugs = cartage.plugins.enabled if plugs.empty? puts 'No active plug-ins.' else plugs.map! { |plug| "* #{plug.plugin_name} (#{plug.version})" } puts <<-plugins Active Plug-ins: #{plugs.join("\n")} plugins end end end end end ``` ## Plug-In Extensions Cartage plug-ins: * are automatically discovered and loaded; * are accessible from and initialized with an owning Cartage instance; * have a namespace in the Cartage configuration file; and * may implement feature requests or offers (see below). When explaining how to build plug-ins, we will be examining three different plug-ins: * Cartage::Manifest manages the `Manifest.txt` file and the `.cartignore` file that indicates what files will be included in the release package, and is for use with the `cartage manifest` family of commands. * No configuration * Bundled with Cartage (`lib/cartage/plugins/manifest.rb`) * Cartage::BuildTarball is responsible for building the final package after Cartage prepares the work area. * No configuration * Implements feature requests * Implements feature offers * Bundled with Cartage (`lib/cartage/plugins/build_tarball.rb`) * Cartage::Bundler is responsible for vendoring Bundler dependencies for Ruby projects. It was originally part of Cartage 1.x, but has been extracted to its own gem. * Has configuration * Implements feature offers * Gem [cartage-bundler][] (`lib/cartage/plugins/bundler.rb`) ### Plug-In Discovery and Initialization Plug-ins are placed in files that are found in `lib/cartage/plugins` and are found in the `$LOAD_PATH`[^1]. The code in these files should define a class that inherits from Cartage::Plugin. When a Cartage instance is created, the plug-in will be loaded and the resulting plug-in class will be added to the instance as a method matching the name of the class of the plug-in. Given an instance of Cartage called `cartage`: * Cartage::Manifest is available as `cartage.manifest`. * Cartage::BuildTarball is available as `cartage.build_tarball`. * Cartage::Bundler is available as `cartage.bundler`. ### Plug-In Configuration Each plug-in has its own configuration dictionary as part of the Cartage::Config dictionary, under the `plugins` dictionary. When the Cartage configuration file is loaded and resolved for Cartage, the relevant section of the Cartage dictionary is provided to the plug-in. Any plug-in's configuration dictionary can be requested through Cartage#config with the `for_plugin` keyword parameter: ```ruby cartage.config(for_plugin: :bundler) ``` Plug-ins should implement `#resolve_plugin_config!` to finalize configuration as appropriate. * Cartage::Manifest only uses core Cartage configuration. * Cartage::BuildTarball only uses core Cartage configuration. * Cartage::Bundler uses its configuration section. For full details, read the documentation of [`cartage-bundler`][]. `#resolve_plugin_config!` resolves the Gemfile and the gem group exclusions. ```yaml --- commands: {} plugins: bundler: without_groups: - assets - development - test ``` This is handled with: ```ruby # Cartage::Bundler is a +vendor_dependencies+ plug-in for Cartage. class Cartage::Bundler < Cartage::Plugin private attr_reader :gemfile, :without_groups def resolve_plugin_config!(config) @gemfile = cartage.root_path.join(config.gemfile || 'Gemfile').expand_path @without_groups = Array(config.without_groups || %w(development test)) end end ``` ### Disabling Plug-Ins All discovered plug-ins are enabled by default and can be disabled in the plug-in's configuration dictionary with the key `disabled`. Note that disabling a plug-in only has meaning for plug-ins that implement feature offers, as disabled plug-ins will be skipped when features are requested. ```yaml --- plugins: bundler: disabled: true ``` Plug-ins may have additional conditions that may cause them to be disabled. Cartage::Bundler is disabled if the `Gemfile` does not exist. ```ruby class Cartage::Bundler < Cartage::Plugin # Cartage::Bundler is only enabled if the Gemfile exists. def disabled? super || !gemfile.exist? end end ``` ### Feature Requests and Offers Cartage plug-ins can implement feature *requests* or *offers* to provide functionality that is loosely-coupled and dynamically discovered. Cartage commands or plug-ins will broadcast feature *requests* to all enabled plug-ins; any plug-in which *offers* that feature will then have appropriate methods called to perform the plug-in's implementation of that feature. A *feature* is simply a label that plug-ins *request* or *offer*. An example is useful, based on Cartage#build_package (run by `cartage pack`). 1. Cartage *requests* `:vendor_dependencies`. Cartage::Bundler *offers* `:vendor_dependencies`. In turn, `#vendor_dependencies` (which ultimately runs `bundle install`) and `#path` will be called on Cartage::Bundler. 2. Cartage *requests* `:pre_build_package`. 3. Cartage *requests* `:build_package`. Cartage::BuildTarball *offers* `:build_package`, so `#build_package` will be called. 4. Cartage::BuildTarball *requests* `:pre_build_tarball`. 5. Cartage::BuildTarball builds the tarball. 6. Cartage::BuildTarball *requests* `:post_build_tarball`. 7. Cartage *requests* `:post_build_package`. There is no fixed set of features, so any plug-in can request any feature. When feature `:build_package` is requested (see [Requesting Features][]), the collection of plugins is filtered for plugins that `#offer?(:build_package)`. #### Offering Features Most plug-ins will *offer* features. The easiest way to offer a particular feature is to implement a public method that matches the name of the requested feature[^2]. This is visible in Cartage::Bundler with `#vendor_dependencies` and Cartage::BuildTarball with `#build_package`. This works because feature *requests* will generally want to call a method of the same name as the feature. The implementation of the `#offer?` test on Cartage::Plugin makes this explicit. ```ruby class Cartage::Plugin def offer?(name) enabled? && offer_feature?(name.to_sym) end private def offer_feature?(name) respond_to?(name) end end ``` #### Requesting Features Some plug-ins will *request* features. As shown in the outline above, Cartage::BuildTarball *offers* the `:build_package` feature but *requests* two additional features, `:pre_build_tarball` and `:post_build_tarball`. It does this with Cartage::Plugins#request. ```ruby class Cartage::BuildTarball < Cartage::Plugin def build_package cartage.plugins.request(:pre_build_tarball) # build the tarball here cartage.plugins.request(:post_build_tarball) end end ``` Cartage::Plugins#request selects plugins that *offer* the feature, and then loops over the selected plug-ins calling the feature method against them. The implementation of #request is flexible enough to allow for a feature to be requested and a different method to be called. This makes Cartage::BuildTarball#build_package implementation look like: ```ruby class Cartage::BuildTarball < Cartage::Plugin def build_package cartage.plugins.request(:pre_build_tarball, :pre_build_tarball) # build the tarball here cartage.plugins.request(:post_build_tarball, :pre_build_tarball) end end ``` This works for a feature that requires multiple phases or steps, like `:vendor_dependencies` does: ```ruby class Cartage private def vendor_dependencies extract_dependency_cache plugins.request(:vendor_dependencies) create_dependency_cache( plugins.request_map(:vendor_dependencies, :path).compact.flatten ) end end ``` 1. Features can be requested using methods other than the name of the feature. `:vendor_dependencies` requires both `#vendor_dependencies` and `#path`. 2. Cartage::Plugins#request_map is used to return the collection of results from the plug-ins *offering* the feature requested. ### *Feature Requests* from Cartage The following *feature requests* are made when running Cartage: #### `:vendor_dependencies` Used during the setup of the work area to install external dependencies in the package directory. Since external dependencies are usually described by a file, a plug-in that offers `:vendor_dependencies` should disable itself if the descriptive file cannot be found (such as the `Gemfile` or `package.json`). > [cartage-bundler][] offers this for Ruby Bundler-based packages. __Methods Required__ * __`#vendor_dependencies`__: Invokes the tool that installs the external dependencies. This might run `bundle install` (for Ruby Bundler) or `npm install` (for Node.js NPM). It should be run in a way that only includes production dependencies, not development dependencies. * __`#path`__: Indicates the directory or directories, relative to the work area, where the external dependencies are installed. This might be `vendor/bundle` (for Ruby Bundler) or `node_modules` (for Node.js NPM). #### `:pre_build_package` Used to perform any other work that must be done for the contents of the package to be ready for packaging. A plug-in could be written to perform asset precompilation prior to packaging, for example. Plug-ins offering `:pre_build_package` __should__ offer `:post_build_package`. __Methods Required__ * __`#pre_build_package`__: Performs the actions required to finalize package preparation. #### `:post_build_package` Used to perform any other work that must be done for the contents of the package to be complete. A plug-in could be written to cryptographically sign built packages, for example. __Methods Required__ * __`#post_build_package`__: Performs the actions required after package creation. If actions should be performed against any created packages, this should request `:build_package#package_name`. ```ruby cartage.plugins.request_map(:build_package, :package_name) ``` #### `:build_package` Used to create a release package. > This offered by Cartage as Cartage::BuildTarball. __Methods Required__ * __`#build_package`__: Performs the actions required to build a package for a particular format. * __`#package_name`__: Returns the name of the package that will be created. #### `:pre_build_tarball` Used to perform any other work that must be done for the contents of the tarball to be ready for packaging. Plug-ins offering `:pre_build_tarball` __should__ offer an implementation of `:post_build_tarball` that reverses the effects of `:pre_build_tarball`. In this way, side-effects between two `:build_package` plug-ins do not occur. > Requested by Cartage::BuildTarball#build_package. __Methods Required__ * __`#pre_build_tarball`__: Performs the actions required before building the tarball. #### `:post_build_tarball` Used to perform any other work that must be after creating the tarball. Recommended to revert actions performed in `:pre_build_tarball` plug-ins. > Requested by Cartage::BuildTarball#build_package. __Methods Required__ * __`#post_build_tarball`__: Performs the actions required after building the tarball. [GLI]: https://github.com/davetron5000/gli [cartage-bundler]: https://github.com/KineticCafe/cartage-bundler [Requesting Features]: #label-Requesting+Features [^1]: Plug-ins are discovered using Rubygems APIs, where commands are discovered using `$LOAD_PATH`. While there is a difference, it has little practical impact. [^2]: This may not be sufficient for all requested features. The `:vendor_dependencies` feature requires both `#vendor_dependencies` and `#path` be implemented.