Commit c60cfc3e authored by Tobias Zimmermann's avatar Tobias Zimmermann
Browse files

Issue #962214 by tstoeckler, sun: Add support for library dependencies.

parent 49de0084
Loading
Loading
Loading
Loading
+1 −0
Changes for CHANGELOG.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line

Libraries 7.x-2.x, xxxx-xx-xx
-----------------------------
#962214 by tstoeckler, sun: Add support for library dependencies.
#1224838 by sun, mjpa: Fix library path not being prepended to JS/CSS files.
#1023258 by tstoeckler: Make 'files' consistently keyed by filename.
#958162 by sun, tstoeckler: Add pre-detect callback group.
+21 −0
Changes for libraries.api.php: 21 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -63,6 +63,27 @@
 *     - css: A list of CSS files to load, using the same syntax as Drupal
 *       core's hook_library().
 *     - php: A list of PHP files to load.
 *   - dependencies: An array of libraries this library depends on. Similar to
 *     declaring module dependencies, the dependency declaration may contain
 *     information on the supported version. Examples of supported declarations:
 *     @code
 *     $libraries['dependencies'] = array(
 *       // Load the 'example' library, regardless of the version available:
 *       'example',
 *       // Only load the 'example' library, if version 1.2 is available:
 *       'example (1.2)',
 *       // Only load a version later than 1.3-beta2 of the 'example' library:
 *       'example (>1.3-beta2)'
 *       // Only load a version equal to or later than 1.3-beta3:
 *       'example (>=1.3-beta3)',
 *       // Only load a version earlier than 1.5:
 *       'example (<1.5)',
 *       // Only load a version equal to or earlier than 1.4:
 *       'example (<=1.4)',
 *       // Combinations of the above are allowed as well:
 *       'example (>=1.3-beta2, <1.5)',
 *     );
 *     @endcode
 *   - variants: (optional) An associative array of available library variants.
 *     For example, the top-level 'files' property may refer to a default
 *     variant that is compressed. If the library also ships with a minified and
+59 −1
Changes for libraries.module: 59 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -261,6 +261,55 @@ function libraries_prepare_files(&$library, $version = NULL, $variant = NULL) {
  }
}

/**
 * Library post-detect callback to process and detect dependencies.
 *
 * It checks whether each of the dependencies of a library are installed and
 * available in a compatible version.
 *
 * @param $library
 *   An associative array of library information or a part of it, passed by
 *   reference.
 * @param $version
 *   If the library information belongs to a specific version, the version
 *   string. NULL otherwise.
 * @param $variant
 *   If the library information belongs to a specific variant, the variant name.
 *   NULL otherwise.
 *
 * @see libraries_info()
 * @see libraries_invoke()
 */
function libraries_detect_dependencies(&$library, $version = NULL, $variant = NULL) {
  if (isset($library['dependencies'])) {
    foreach ($library['dependencies'] as &$dependency_string) {
      $dependency_info = drupal_parse_dependency($dependency_string);
      $dependency = libraries_detect($dependency_info['name']);
      if (!$dependency['installed']) {
        $library['installed'] = FALSE;
        $library['error'] = 'missing dependency';
        $library['error message'] = t('The %dependency library, which the %library library depends on, is not installed.', array(
          '%dependency' => $dependency['name'],
          '%library' => $library['name'],
        ));
      }
      elseif (drupal_check_incompatibility($dependency_info, $dependency['version'])) {
        $library['installed'] = FALSE;
        $library['error'] = 'incompatible dependency';
        $library['error message'] = t('The version %dependency_version of the %dependency library is not compatible with the %library library.', array(
          '%dependency_version' => $dependency['version'],
          '%dependency' => $dependency['name'],
          '%library' => $library['name'],
        ));
      }

      // Remove the version string from the dependency, so libraries_load() can
      // load the libraries directly.
      $dependency_string = $dependency_info['name'];
    }
  }
}

/**
 * Returns information about registered libraries.
 *
@@ -339,6 +388,7 @@ function libraries_info_defaults(&$library, $name) {
    'version callback' => 'libraries_get_version',
    'version arguments' => array(),
    'files' => array(),
    'dependencies' => array(),
    'variants' => array(),
    'versions' => array(),
    'integration files' => array(),
@@ -352,7 +402,8 @@ function libraries_info_defaults(&$library, $name) {
  );

  // Add our own callbacks before any others.
  $library['callbacks']['info'] = array_merge(array('libraries_prepare_files'), $library['callbacks']['info']);
  array_unshift($library['callbacks']['info'], 'libraries_prepare_files');
  array_unshift($library['callbacks']['post-detect'], 'libraries_detect_dependencies');

  return $library;
}
@@ -538,6 +589,13 @@ function libraries_load($name, $variant = NULL) {
    // If the library (variant) is installed, load it.
    $library['loaded'] = FALSE;
    if ($library['installed']) {
      // Load library dependencies.
      if (isset($library['dependencies'])) {
        foreach ($library['dependencies'] as $dependency) {
          libraries_load($dependency);
        }
      }

      // Invoke callbacks in the 'load' group.
      libraries_invoke('load', $library);

+86 −0
Changes for tests/libraries.test: 86 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -50,6 +50,78 @@ class LibrariesTestCase extends DrupalWebTestCase {
    libraries_prepare_files($library, NULL, NULL);
    $this->assertEqual($expected, $library, 'libraries_prepare_files() works correctly.');

    // Test libraries_detect_dependencies().
    $library = array(
      'name' => 'Example',
      'dependencies' => array('example_missing'),
    );
    libraries_detect_dependencies($library);
    $this->assertEqual($library['error'], 'missing dependency', 'libraries_detect_dependencies() detects missing dependency');
    $error_message = t('The %dependency library, which the %library library depends on, is not installed.', array(
      '%dependency' => 'Example missing',
      '%library' => $library['name'],
    ));
    $this->verbose("Expected:<br>$error_message");
    $this->verbose('Actual:<br>' . $library['error message']);
    $this->assertEqual($library['error message'], $error_message, 'Correct error message for a missing dependency');
    $version = '1.1';
    $compatible = array(
      '1.1',
      '<=1.1',
      '>=1.1',
      '<1.2',
      '<2.0',
      '>1.0',
      '>1.0-rc1',
      '>1.0-beta2',
      '>1.0-alpha3',
      '>0.1',
      '<1.2, >1.0',
      '>0.1, <=1.1',
    );
    $incompatible = array(
      '1.2',
      '2.0',
      '<1.1',
      '>1.1',
      '<=1.0',
      '<=1.0-rc1',
      '<=1.0-beta2',
      '<=1.0-alpha3',
      '>=1.2',
      '<1.1, >0.9',
      '>=0.1, <1.1',
    );
    $library = array(
      'name' => 'Example',
    );
    foreach ($compatible as $version_string) {
      $library['dependencies'][0] = "example_dependency ($version_string)";
      // libraries_detect_dependencies() is a post-detect callback, so
      // 'installed' is already set, when it is called. It sets the value to
      // FALSE for missing or incompatible dependencies.
      $library['installed'] = TRUE;
      libraries_detect_dependencies($library);
      $this->assertTrue($library['installed'], "libraries_detect_dependencies() detects compatible version string: '$version_string' is compatible with '$version'");
    }
    foreach ($incompatible as $version_string) {
      $library['dependencies'][0] = "example_dependency ($version_string)";
      $library['installed'] = TRUE;
      unset($library['error'], $library['error message']);
      libraries_detect_dependencies($library);
      $this->assertEqual($library['error'], 'incompatible dependency', "libraries_detect_dependencies() detects incompatible version strings: '$version_string' is incompatible with '$version'");
    }
    // Instead of repeating this assertion for each version string, we just
    // re-use the $library variable from the foreach loop.
    $error_message = t('The version %dependency_version of the %dependency library is not compatible with the %library library.', array(
      '%dependency_version' => $version,
      '%dependency' => 'Example dependency',
      '%library' => $library['name'],
    ));
    $this->verbose("Expected:<br>$error_message");
    $this->verbose('Actual:<br>' . $library['error message']);
    $this->assertEqual($library['error message'], $error_message, 'Correct error message for an incompatible dependency');

    // Test that library information is found correctly.
    $expected = array(
      'name' => 'Example files',
@@ -157,6 +229,20 @@ class LibrariesTestCase extends DrupalWebTestCase {
    $this->verbose('<pre>' . var_export($library, TRUE) . '</pre>');
    $this->assertEqual($library['variants']['example_variant']['installed'], TRUE, 'Existing variant found.');

    // Test dependencies.
    $library = libraries_load('example_dependency_missing');
    $this->verbose('<pre>' . var_export($library, TRUE) . '</pre>');
    $this->assertFalse($library['loaded'], 'Library with missing dependency cannot be loaded');
    $library = libraries_load('example_dependency_incompatible');
    $this->verbose('<pre>' . var_export($library, TRUE) . '</pre>');
    $this->assertFalse($library['loaded'], 'Library with incompatible dependency cannot be loaded');
    $library = libraries_load('example_dependency_compatible');
    $this->verbose('<pre>' . var_export($library, TRUE) . '</pre>');
    $this->assertEqual($library['loaded'], 1, 'Library with compatible dependency is loaded');
    $loaded = &drupal_static('libraries_load');
    $this->verbose('<pre>' . var_export($loaded, TRUE) . '</pre>');
    $this->assertEqual($loaded['example_dependency']['loaded'], 1, 'Dependency library is also loaded');

    // Test the applying of callbacks.
    $expected = array(
      'name' => 'Example callback',
+32 −0
Changes for tests/libraries_test.module: 32 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -200,6 +200,38 @@ function libraries_test_libraries_info() {
    ),
  );

  // Test dependency loading.
  // We add one file to each library to be able to verify if it was loaded with
  // libraries_load().
  // This library acts as a dependency for the libraries below.
  $libraries['example_dependency'] = array(
    'name' => 'Example dependency',
    'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
    'version' => '1.1',
    'files' => array('js' => array('example_1.js')),
  );
  $libraries['example_dependency_missing'] = array(
    'name' => 'Example dependency missing',
    'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
    'version' => '1',
    'dependencies' => array('example_missing'),
    'files' => array('js' => array('example_1.js')),
  );
  $libraries['example_dependency_incompatible'] = array(
    'name' => 'Example dependency incompatible',
    'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
    'version' => '1',
    'dependencies' => array('example_dependency (>1.1)'),
    'files' => array('js' => array('example_1.js')),
  );
  $libraries['example_dependency_compatible'] = array(
    'name' => 'Example dependency compatible',
    'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
    'version' => '1',
    'dependencies' => array('example_dependency (>=1.1)'),
    'files' => array('js' => array('example_1.js')),
  );

  // Test the applying of callbacks.
  $libraries['example_callback'] = array(
    'name' => 'Example callback',