diff --git a/core/core.services.yml b/core/core.services.yml
index a7997ea66b..ab4281a47e 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -288,6 +288,12 @@ services:
     tags:
       - { name: page_cache_response_policy }
       - { name: dynamic_page_cache_response_policy }
+  composer.project_root:
+    class: Drupal\Core\Composer\ProjectRoot
+    arguments: ['@app.root']
+  composer.dev_dependency_finder:
+    class: Drupal\Core\Composer\Dependencies\DevDependencyFinder
+    arguments: ['@composer.project_root', '@string_translation']
   config.manager:
     class: Drupal\Core\Config\ConfigManager
     arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher']
diff --git a/core/lib/Drupal/Core/Composer/Dependencies/DevDependencyFinder.php b/core/lib/Drupal/Core/Composer/Dependencies/DevDependencyFinder.php
new file mode 100644
index 0000000000..449e144e99
--- /dev/null
+++ b/core/lib/Drupal/Core/Composer/Dependencies/DevDependencyFinder.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\Core\Composer\Dependencies;
+
+use Drupal\Core\Composer\ProjectRootInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Builds hook_requirements() info about Composer dev dependencies.
+ *
+ * @internal
+ */
+class DevDependencyFinder {
+
+  use StringTranslationTrait;
+
+  /**
+   * The Composer project root service.
+   *
+   * @var \Drupal\Core\Composer\ProjectRootInterface
+   */
+  protected $projectRoot;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Composer\ProjectRootInterface $project_root
+   *   The Composer project root service.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(ProjectRootInterface $project_root, TranslationInterface $string_translation) {
+    $this->projectRoot = $project_root;
+    $this->setStringTranslation($string_translation);
+  }
+
+  /**
+   * Builds the Composer dev warning for the Drupal installation.
+   *
+   * @return array[]
+   *   Requirements array, suitable for hook_requirements().
+   *
+   * @see hook_requirements()
+   */
+  public function buildRequirements() {
+    $installed_dev = [];
+    // Get a list of installed packages.
+    $installed = array_keys($this->projectRoot->getInstalledPackages());
+    // Get the dev requirements for the root project.
+    $dev_requirements = array_keys($this->projectRoot->getDevRequirements());
+
+    foreach ($dev_requirements as $dev_requirement) {
+      if (in_array($dev_requirement, $installed)) {
+        // @todo: Check version constraints for fewer false positives.
+        $installed_dev[] = $dev_requirement;
+      }
+    }
+
+    $requirements = [
+      'composer_dev_dependencies' => [
+        'title' => $this->t('Composer dev requirements'),
+      ],
+    ];
+    // If we have the same number of installed dev packages, then tell the user
+    // that they might need to change their site build workflow.
+    if (!empty($installed_dev) && (count($installed_dev) == count($dev_requirements))) {
+      $requirements['composer_dev_dependencies'] += [
+        'description' => $this->t('This Drupal installation appears to include development packages. This can lead to performance and security issues. Read the <a href=":documentation">documentation on drupal.org</a>', [
+          ':documentation' => 'https://www.drupal.org/docs/develop/using-composer',
+        ]),
+        'severity' => REQUIREMENT_WARNING,
+      ];
+    }
+    else {
+      $requirements['composer_dev_dependencies'] += [
+        'description' => $this->t('This Drupal installation does not appear to have development packages installed. <a href=":documentation">Documentation on drupal.org</a>', [
+          ':documentation' => 'https://www.drupal.org/docs/develop/using-composer',
+        ]),
+        'severity' => REQUIREMENT_OK,
+      ];
+    }
+
+    return $requirements;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Composer/ProjectRoot.php b/core/lib/Drupal/Core/Composer/ProjectRoot.php
new file mode 100644
index 0000000000..3c1e8a84d5
--- /dev/null
+++ b/core/lib/Drupal/Core/Composer/ProjectRoot.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Drupal\Core\Composer;
+
+/**
+ * Allows introversion of installed Composer dependencies.
+ */
+class ProjectRoot implements ProjectRootInterface {
+
+  /**
+   * The full path to the root directory of the Drupal installation.
+   *
+   * @var string
+   */
+  protected $appRoot;
+
+  /**
+   * The full path to the vendor directory for this Drupal installation.
+   * @var string
+   */
+  protected $vendorDirectory;
+
+  public function __construct($app_root) {
+    $this->appRoot = $app_root;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProjectVendorDirectory() {
+    if (empty($this->vendorDirectory)) {
+      $vendor_dir = $this->appRoot . '/vendor';
+      $autoload = require $this->appRoot . '/autoload.php';
+      if ($autoload instanceof \Composer\Autoload\ClassLoader) {
+        $ref_loader = new \ReflectionClass($autoload);
+        $vendor_dir = dirname(dirname($ref_loader->getFileName()));
+      }
+      $this->vendorDirectory = $vendor_dir;
+    }
+    return $this->vendorDirectory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInstalledPackages() {
+    $installed_json_path = $this->getProjectVendorDirectory() . '/composer/installed.json';
+
+    // If installed.json does not exist, no packages are installed.
+    if (!file_exists($installed_json_path)) {
+      return [];
+    }
+
+    $installed_packages = [];
+    $packages = [];
+
+    $json = file_get_contents($installed_json_path, FALSE);
+    if ($json !== FALSE) {
+      $packages = json_decode($json);
+    }
+    if (!empty($packages)) {
+      foreach ($packages as $package) {
+        if (isset($package->name) && isset($package->version_normalized)) {
+          $installed_packages[$package->name] = $package->version_normalized;
+        }
+      }
+    }
+    return $installed_packages;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDevRequirements() {
+    // Since the default setup for Drupal is to have both a root package and a
+    // pre-installed package for drupal/core, we have to merge requirements to
+    // emulate the composer-merge plugin.
+    // @todo: Possibly change this when the drupal/core subtree split happens in
+    // https://www.drupal.org/node/2352091
+    return array_merge(
+      $this->getDevRequires($this->appRoot . '/composer.json'),
+      $this->getDevRequires($this->appRoot . '/core/composer.json')
+    );
+  }
+
+  /**
+   * Gathers the dev dependencies from a composer.json file.
+   *
+   * This method should never throw exceptions, since there are many different
+   * ways to structure Drupal as a Composer-based project.
+   *
+   * @param string $composer_file
+   *   Path to the composer.json file to read in.
+   *
+   * @return string[]
+   *   Associative array where the keys are the package names and the values are
+   *   version constraints. This is similar to the composer.json file's
+   *   require-dev array.
+   *
+   * @see https://getcomposer.org/doc/04-schema.md#require-dev
+   */
+  protected function getDevRequires($composer_file) {
+    $json = [];
+    $contents = file_get_contents($composer_file, FALSE);
+    if ($contents !== FALSE) {
+      $json = json_decode($contents, TRUE);
+    }
+    if (isset($json['require-dev'])) {
+      return $json['require-dev'];
+    }
+    return [];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Composer/ProjectRootInterface.php b/core/lib/Drupal/Core/Composer/ProjectRootInterface.php
new file mode 100644
index 0000000000..981b09b9f6
--- /dev/null
+++ b/core/lib/Drupal/Core/Composer/ProjectRootInterface.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\Core\Composer;
+
+/**
+ * Allows introversion of installed Composer dependencies.
+ *
+ * @todo Some of this would very well be changed in
+ *   https://www.drupal.org/node/2494073
+ */
+interface ProjectRootInterface {
+
+  /**
+   * Gets the vendor directory for the project.
+   *
+   * @return string
+   *   Absolute path to the vendor directory.
+   */
+  public function getProjectVendorDirectory();
+
+  /**
+   * Gets Composer packages that are currently installed.
+   *
+   * @return string[]
+   *   An associative array of the packages which were installed by Composer.
+   *   Keys are package names and values are package versions. Example:
+   *   ['psr/log' => '1.0.2']. Returns an empty array if no packages
+   *   have been installed or the installed.json file could not be found.
+   */
+  public function getInstalledPackages();
+
+  /**
+   * Gets the development requirements for the Drupal installation.
+   *
+   * Note that since Drupal uses Wikimedia's composer-merge plugin, this method
+   * will return a combination of the root package and the drupal/core package.
+   *
+   * This method only deals with core's dev requirements, and does not include
+   * dev requirements for contrib. It's likely that if core's dev requirements
+   * are installed, then contrib's dev requirements are also installed.
+   *
+   * @return string[]
+   *   Associative array where the keys are the package names and the values are
+   *   version constraints. This is similar to the composer.json file's
+   *   require-dev array.
+   *
+   * @see https://getcomposer.org/doc/04-schema.md#require-dev
+   */
+  public function getDevRequirements();
+
+}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 3c8332e367..46f23f0f63 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -73,6 +73,12 @@ function system_requirements($phase) {
         'severity' => REQUIREMENT_WARNING,
       ];
     }
+
+    // Add warning about Composer-based dev requirements.
+    $requirements = array_merge(
+      $requirements,
+      \Drupal::service('composer.dev_dependency_finder')->buildRequirements()
+    );
   }
 
   // Web server information.
diff --git a/core/tests/Drupal/KernelTests/Core/Composer/Dependencies/DevDependencyFinderTest.php b/core/tests/Drupal/KernelTests/Core/Composer/Dependencies/DevDependencyFinderTest.php
new file mode 100644
index 0000000000..85e340d246
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Composer/Dependencies/DevDependencyFinderTest.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Composer\Dependencies;
+
+use Drupal\Core\Composer\Dependencies\DevDependencyFinder;
+use Drupal\Core\Composer\ProjectRootInterface;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Composer\Dependencies\DevDependencyFinder
+ * @group Composer
+ * @group Dependencies
+ *
+ * @internal
+ *
+ * @todo Make this a UnitTestBase test when it no longer has dependencies on
+ *   install.inc.
+ */
+class DevDependencyFinderTest extends KernelTestBase {
+
+  public function provideBuildRequirements() {
+    // Data provider methods are run before setUp(), so they can't use
+    // constants from *.inc files. REQUIREMENT_OK = 0, REQUIREMENT_WARNING = 1.
+    return [
+      [0, [], []],
+      [0, ['any_requirement' => '0.0.1'], []],
+      [1, ['dev_requirement' => '0.0.1'], ['dev_requirement' => '0.0.*']],
+    ];
+  }
+
+  /**
+   * @covers ::buildRequirements
+   * @dataProvider provideBuildRequirements
+   */
+  public function testBuildRequirements($expected, $installed, $dev_requirements) {
+    $mock_root = $this->getMockBuilder(ProjectRootInterface::class)
+      ->setMethods(['getInstalledPackages', 'getDevRequirements'])
+      ->getMockForAbstractClass();
+    $mock_root->expects($this->once())
+      ->method('getInstalledPackages')
+      ->willReturn($installed);
+    $mock_root->expects($this->once())
+      ->method('getDevRequirements')
+      ->willReturn($dev_requirements);
+
+    $finder = new DevDependencyFinder(
+      $mock_root,
+      $this->getMockForAbstractClass(TranslationInterface::class)
+    );
+    $requirements = $finder->buildRequirements();
+
+    $this->assertArrayHasKey('composer_dev_dependencies', $requirements);
+    foreach (['title', 'description', 'severity'] as $property) {
+      $this->assertArrayHasKey($property, $requirements['composer_dev_dependencies']);
+    }
+    $this->assertEquals($expected, $requirements['composer_dev_dependencies']['severity']);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Composer/ProjectRootTest.php b/core/tests/Drupal/KernelTests/Core/Composer/ProjectRootTest.php
new file mode 100644
index 0000000000..34ad213f15
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Composer/ProjectRootTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Composer;
+
+use Drupal\Core\Composer\ProjectRoot;
+use Drupal\KernelTests\KernelTestBase;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Composer\ProjectRoot
+ * @group Composer
+ *
+ * @internal
+ */
+class ProjectRootTest extends KernelTestBase {
+
+  /**
+   * Exercise the code in getProjectVendorDirectory().
+   *
+   * We don't have a good way to unit test vendor discovery, so we must rely on
+   * the behavior of a semi-installed Drupal site.
+   *
+   * @covers ::getProjectVendorDirectory
+   */
+  public function testGetProjectVendorDirectory() {
+    $root = new ProjectRoot($this->container->get('app.root'));
+    $vendor_root = $root->getProjectVendorDirectory();
+    $this->assertFileExists($vendor_root);
+    foreach(['symfony', 'composer'] as $dir) {
+      $this->assertFileExists($vendor_root . '/' . $dir);
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Composer/ProjectRootTest.php b/core/tests/Drupal/Tests/Core/Composer/ProjectRootTest.php
new file mode 100644
index 0000000000..716cfd7073
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Composer/ProjectRootTest.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\Tests\Core\Composer;
+
+use Drupal\Core\Composer\ProjectRoot;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Composer\ProjectRoot
+ * @group Composer
+ *
+ * @internal
+ */
+class ProjectRootTest extends UnitTestCase {
+
+  /**
+   * Generate a mock ProjectRoot object for testing.
+   *
+   * Since we can't unit test getProjectVendorDirectory(), we have to mock it so
+   * that it returns a reasonable value. This allows us to test the rest of the
+   * class.
+   *
+   * @param string $root
+   *   The root directory of the vfsStream mock.
+   */
+  protected function getMockProjectRoot($root) {
+    $root = $this->getMockBuilder(ProjectRoot::class)
+      ->setMethods(['getProjectVendorDirectory'])
+      ->setConstructorArgs([vfsStream::url($root)])
+      ->getMock();
+    $root->expects($this->once())
+      ->method('getProjectVendorDirectory')
+      ->willReturn(vfsStream::url($root) . '/vendor');
+  }
+
+  public function provideEmptyInstalledJson() {
+    return [
+      ['no_json' => ['vendor' => ['composer' => []]]],
+      ['empty_json' => ['vendor' => ['composer' => ['installed.json' => ' ']]]],
+      ['empty_json_array' => ['vendor' => ['composer' => ['installed.json' => '[]']]]],
+      ['bad_json' => ['vendor' => ['composer' => ['installed.json' => '[{"key": "value"}]']]]],
+    ];
+  }
+
+  /**
+   * Verify behavior for missing or bad installed.json.
+   *
+   * @covers ::getInstalledPackages
+   * @dataProvider provideEmptyInstalledJson
+   */
+  public function testGetInstalledPackagesEmpty($filesystem) {
+    vfsStream::setup('root_test', NULL, $filesystem);
+    $root = $this->getMockProjectRoot('root_test');
+    $this->assertSame([], $root->getInstalledPackages());
+  }
+
+  public function provideInstalledJson() {
+    return [
+      [
+        ['package_name' => 'package_version'],
+        ['vendor' => ['composer' => ['installed.json' => '[{"name": "package_name", "version_normalized": "package_version"}]']]],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::getInstalledPackages
+   * @dataProvider provideInstalledJson
+   */
+  public function testGetInstalledPackages($expected, $filesystem) {
+    vfsStream::setup('root_test', NULL, $filesystem);
+    $root = $this->getMockProjectRoot('root_test');
+    $this->assertArrayEquals($expected, $root->getInstalledPackages());
+  }
+
+}
