bcmath_compat: can't install on heroku

During an Heroku deployment i get this error :

Your requirements could not be resolved to an installable set of packages.

Problem 1
- don't install phpseclib/bcmath_compat 1.0.3|don't install php 7.3.13
- composer.json/composer.lock requires php 7.3.* -> satisfiable by php[7.3.13].
- Installation request for phpseclib/bcmath_compat 1.0.3 -> satisfiable by phpseclib/bcmath_compat[1.0.3].

The phpseclib/bcmath_compat package is an indirect dependency of laravel/framework via moontoast/math

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 5
  • Comments: 53 (19 by maintainers)

Commits related to this issue

Most upvoted comments

@calvinmuller run composer require phpseclib/bcmath_compat:1.0.4 which locks the version at 1.0.4 which contained the fix (removed in 1.0.5)

Here is the Heroku support response :

The issue here is your update of moontoast/math from 1.1.2 to 1.2.1

1.1.2 required ext-bcmath: https://packagist.org/packages/moontoast/math#1.1.2

1.2.1 uses phpseclib/bcmath_compat instead: https://packagist.org/packages/moontoast/math#1.2.1

The issue that follows then is that phpseclib/bcmath_compat via its composer.json metadata provides the ext-bcmath extension, but that extension is marked as replaced (because it’s bundled with PHP) by PHP 7.3.13.

A replace is a provide plus a conflict, and that’s why PHP 7.3.13 (with ext-bcmath available as a shared extension) conflicts.

This doesn’t happen on your local machine because dependency resolution works differently there - your regular composer install step is working, but on Heroku, what’s failing is the ā€œplatform installā€ step that installs PHP and all necessary extensions, not userland dependencies, using a special custom repository and installer, and a composer.json generated from your composer.lock.

I don’t think there is a way to work around this, short of you transitioning to e.g. brick/math (moontoast/math is marked as abandoned), or downgrading to moontoast/math 1.1.2. I’ll put it on the list of things to investigate on our side, but it’s a very fundamental aspect of how Composer handles dependencies, and how we have to resolve app requirements, that make it an edge case we’ll not likely be able to fix.

Hope this helps, and sorry I don’t have happier news.

Hi.

To clarify what’s going on on Heroku, and why this is failing… this is not happening during the regular composer install, but during the installation of PHP (and extensions). The ā€œmainā€ installation of app dependencies is not custom, and uses a standard Composer install without proprietary changes.

But before that step can even be done, we need PHP installed, and all the right extensions that the app (and its dependencies) require, otherwise composer install would immediately fail.

To achieve that on Heroku, we parse the app’s composer.lock, extract all packages with platform dependencies (php, ext-foobar, etc), and translate that into a new composer.json where the dependencies are like heroku-sys/php, heroku-sys/ext-foobar, and so forth).

This composer.json is then composer installed (against a custom repository, e.g. https://lang-php.s3.amazonaws.com/dist-heroku-18-stable/packages.json and using a custom installer) to install binaries of PHP and any required extensions.

The reason it’s done this way is that dependency graphs for platform package requirements in a composer.json can be complex, and the only way to ensure we’re installing the right thing(s) is to mirror the same package structure and requirements as the main project, otherwise we’d always run into situations where the later composer install for the app dependencies fails.

Example: your project requires php:* and ext-redis:*, that should give you PHP 7.3.latest and ext-redis 5.latest.

But if your project requires php:* and ext-redis:3.*, you’ll get PHP 7.2, since ext-redis v3 is not compatible with 7.3.

Multiply that by the number of php and ext-… require and conflict rules in all dependent composer.json files, and it should be clear why some hand-written logic that first tries to install the latest PHP 7.3 and then just fails in the next step (when trying to install ext-redis v5) would constantly lead to frustration for users - we want a git push heroku master to ā€œjust workā€ by figuring out the right set of things to install.

Now the issue with ext-bcmath is that it’s built into PHP, and the package manifest for each PHP build (heroku-sys/php package) on Heroku contains heroku-sys/ext-bcmath as a replace declaration, because provide would be incorrect - we want to avoid the installation of an ā€œexternalā€ ext-bcmath (e.g. when someone builds one into a custom ā€œplatformā€ repository for use on Heroku).

This is, however, only really a requirement for extensions that are not built as shared (and ext-bcmath is built as shared). I’ll investigate whether the package definitions could be changed in such a way that shared extensions are provided instead, but IIRC there was a particular reason (internal to Composer) why we went with replace for everything back when this was implemented.

Example generated ā€œplatformā€ composer.json (from an app’s composer.lock):

{
  "config": {
    "cache-files-ttl": 0,
    "discard-changes": true
  },
  "minimum-stability": "stable",
  "prefer-stable": false,
  "provide": {
    "heroku-sys/cedar": "14.2020.01.10"
  },
  "require": {
    "composer.json/composer.lock": "dev-44c755d72e579138785ca11db85fb0f2",
    "consoletvs/invoices": "1.5.0",
    "moontoast/math": "1.2.1",
    "phpseclib/bcmath_compat": "1.0.3",
    "phpseclib/phpseclib": "2.0.23",
    "heroku-sys/apache": "^2.4.10",
    "heroku-sys/nginx": "^1.8.0"
  },
  "require-dev": {},
  "repositories": [
    {
      "packagist": false
    },
    {
      "type": "path",
      "url": "…/support/installer/",
      "options": {
        "symlink": false
      }
    },
    {
      "type": "composer",
      "url": "https://lang-php.s3.amazonaws.com/dist-heroku-18-stable/"
    },
    {
      "type": "package",
      "package": [
        {
          "type": "metapackage",
          "name": "consoletvs/invoices",
          "version": "1.5.0",
          "require": {
            "heroku-sys/ext-bcmath": "*"
          },
          "replace": {},
          "provide": {},
          "conflict": {}
        },
        {
          "type": "metapackage",
          "name": "moontoast/math",
          "version": "1.2.1",
          "require": {
            "heroku-sys/php": ">=5.3.3"
          },
          "replace": {},
          "provide": {},
          "conflict": {}
        },
        {
          "type": "metapackage",
          "name": "phpseclib/bcmath_compat",
          "version": "1.0.3",
          "require": {},
          "replace": {},
          "provide": {
            "heroku-sys/ext-bcmath": "7.3.5"
          },
          "conflict": {}
        },
        {
          "type": "metapackage",
          "name": "phpseclib/phpseclib",
          "version": "2.0.23",
          "require": {
            "heroku-sys/php": ">=5.3.3"
          },
          "replace": {},
          "provide": {},
          "conflict": {}
        },
        {
          "type": "metapackage",
          "name": "composer.json/composer.lock",
          "version": "dev-44c755d72e579138785ca11db85fb0f2",
          "require": {
            "heroku-sys/php": "7.3.*"
          },
          "replace": {},
          "provide": {},
          "conflict": {}
        }
      ]
    }
  ]
}

Example package definition for PHP 7.3.13 from the custom Heroku repository for platform installation:

{
  "conflict": {
    "heroku-sys/hhvm": "*"
  },
  "dist": {
    "type": "heroku-sys-tar",
    "url": "https://lang-php.s3.amazonaws.com/dist-heroku-18-stable/php-7.3.13.tar.gz"
  },
  "extra": {
    "export": "bin/export.php.sh",
    "profile": "bin/profile.php.sh",
    "shared": {
      "heroku-sys/ext-bcmath": true,
      "heroku-sys/ext-calendar": true,
      "heroku-sys/ext-exif": true,
      "heroku-sys/ext-ftp": true,
      "heroku-sys/ext-gd": true,
      "heroku-sys/ext-gettext": true,
      "heroku-sys/ext-gmp": true,
      "heroku-sys/ext-imap": true,
      "heroku-sys/ext-intl": true,
      "heroku-sys/ext-ldap": true,
      "heroku-sys/ext-mbstring": true,
      "heroku-sys/ext-pcntl": true,
      "heroku-sys/ext-pdo_sqlite": true,
      "heroku-sys/ext-shmop": true,
      "heroku-sys/ext-soap": true,
      "heroku-sys/ext-sodium": true,
      "heroku-sys/ext-sqlite3": true,
      "heroku-sys/ext-xmlrpc": true,
      "heroku-sys/ext-xsl": true
    }
  },
  "name": "heroku-sys/php",
  "provide": {},
  "replace": {
    "heroku-sys/ext-PDO": "self.version",
    "heroku-sys/ext-Phar": "self.version",
    "heroku-sys/ext-Reflection": "self.version",
    "heroku-sys/ext-SPL": "self.version",
    "heroku-sys/ext-SimpleXML": "self.version",
    "heroku-sys/ext-Zend-OPcache": "self.version",
    "heroku-sys/ext-bcmath": "self.version",
    "heroku-sys/ext-bz2": "self.version",
    "heroku-sys/ext-calendar": "self.version",
    "heroku-sys/ext-ctype": "self.version",
    "heroku-sys/ext-curl": "self.version",
    "heroku-sys/ext-date": "self.version",
    "heroku-sys/ext-dom": "20031129",
    "heroku-sys/ext-exif": "self.version",
    "heroku-sys/ext-fileinfo": "self.version",
    "heroku-sys/ext-filter": "self.version",
    "heroku-sys/ext-ftp": "self.version",
    "heroku-sys/ext-gd": "self.version",
    "heroku-sys/ext-gettext": "self.version",
    "heroku-sys/ext-gmp": "self.version",
    "heroku-sys/ext-hash": "self.version",
    "heroku-sys/ext-iconv": "self.version",
    "heroku-sys/ext-imap": "self.version",
    "heroku-sys/ext-intl": "self.version",
    "heroku-sys/ext-json": "1.7.0",
    "heroku-sys/ext-ldap": "self.version",
    "heroku-sys/ext-libxml": "self.version",
    "heroku-sys/ext-mbstring": "self.version",
    "heroku-sys/ext-mysqli": "self.version",
    "heroku-sys/ext-mysqlnd": "0",
    "heroku-sys/ext-openssl": "self.version",
    "heroku-sys/ext-pcntl": "self.version",
    "heroku-sys/ext-pcre": "self.version",
    "heroku-sys/ext-pdo_mysql": "self.version",
    "heroku-sys/ext-pdo_pgsql": "self.version",
    "heroku-sys/ext-pdo_sqlite": "self.version",
    "heroku-sys/ext-pgsql": "self.version",
    "heroku-sys/ext-posix": "self.version",
    "heroku-sys/ext-readline": "self.version",
    "heroku-sys/ext-session": "self.version",
    "heroku-sys/ext-shmop": "self.version",
    "heroku-sys/ext-soap": "self.version",
    "heroku-sys/ext-sockets": "self.version",
    "heroku-sys/ext-sodium": "self.version",
    "heroku-sys/ext-sqlite3": "self.version",
    "heroku-sys/ext-tokenizer": "self.version",
    "heroku-sys/ext-xml": "self.version",
    "heroku-sys/ext-xmlreader": "self.version",
    "heroku-sys/ext-xmlrpc": "self.version",
    "heroku-sys/ext-xmlwriter": "self.version",
    "heroku-sys/ext-xsl": "self.version",
    "heroku-sys/ext-zip": "1.15.4",
    "heroku-sys/ext-zlib": "self.version",
    "heroku-sys/php-64bit": "self.version"
  },
  "require": {
    "heroku-sys/heroku": "^18.0.0",
    "heroku/installer-plugin": "^1.2.0"
  },
  "time": "2019-12-19 17:44:37",
  "type": "heroku-sys-php",
  "version": "7.3.13"
}

As a temporary workaround, you can use Composer’s require inline alias feature to force the use of a previous, non-breaking version of moontoast/math.

Simply run the following command at the root of your project:

composer require moontoast/math "1.1.2 as 1.999.999"

I still stand by my statement that this is a Heroku bug.

Pinging @dzuelke. Is this something you’re able to look into, to rule out whether this is a Heroku issue? šŸ˜„

I think what I’ll do is this:

When I get home I’ll tag a new release of bcmath_compat: 1.0.4. In this new release I’ll remove the ā€œprovidesā€ bit from composer.json. In theory it could break other setups but tbh Laravel Nova is probably accounting for 99% of the usage of this library (altho I have no idea what percentage of Laravel Nova installs are running on Heroku).

Maybe I’ll create a 2.0.0 tag as well wherein the ā€œprovidesā€ will be included.

Then when I update bcmath_compat to use phpseclib 3.0 I’ll create a 3.0.0 tag and a 4.0.0 tag.

It’s a suboptimal solution but I think it’s the most practical one available to us idk.

@dzuelke - great news!

I’ve unpinned the issue and updated the README.md file to remove the Heroku note.

Thanks!

User-land dependency installation on Heroku supports Composer 2, but the installation of the platform packages themselves (PHP, extensions, etc), where the error is occurring, is still performed using Composer 1 at the moment, so this is expected.

I’ve tagged a new release - 1.0.4 - that removes the provides thing from composer.json.

I still stand by my statement that this is a Heroku bug. It sounds like they’re using a custom Composer setup. If Heroku wants to continue to use their custom Composer setup I think they should do one of the following:

Either way this is not a fix that should live in bcmath_compat but this also seems to me like a bit of a David and Goliath situation, with bcmath_compat being David and Heroku being Goliath, so whatever.

Just to loop back, I decided to cut my losses and revert to a last known good commit. From there, I branched. I then:

composer require moontoast/math ā€œ1.1.2 as 1.999.999ā€

Followed by a composer update

I then updated Nova, and Spark, and again ran my composer update. I then pushed to Heroku.

I can’t say for sure why, but it worked so it’s possible my issues are related to my commits. If I can come up with a better answer than that, I’ll drop back.

Thanks all again for your help.