Skip to content

Commit

Permalink
feat: Elixir support (#4496)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov authored and rarkins committed Oct 4, 2019
1 parent 36b9c4a commit 982896d
Show file tree
Hide file tree
Showing 23 changed files with 813 additions and 89 deletions.
30 changes: 30 additions & 0 deletions Dockerfile
Expand Up @@ -74,6 +74,31 @@ RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \

## END copy Node.js

# Erlang

RUN cd /tmp && \
curl https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb -o erlang-solutions_1.0_all.deb && \
dpkg -i erlang-solutions_1.0_all.deb && \
rm -f erlang-solutions_1.0_all.deb

ENV ERLANG_VERSION=22.0.2-1

RUN apt-get update && \
apt-cache policy esl-erlang && \
apt-get install -y esl-erlang=1:$ERLANG_VERSION && \
apt-get clean

# Elixir

ENV ELIXIR_VERSION 1.8.2

RUN curl -L https://github.com/elixir-lang/elixir/releases/download/v${ELIXIR_VERSION}/Precompiled.zip -o Precompiled.zip && \
mkdir -p /opt/elixir-${ELIXIR_VERSION}/ && \
unzip Precompiled.zip -d /opt/elixir-${ELIXIR_VERSION}/ && \
rm Precompiled.zip

ENV PATH $PATH:/opt/elixir-${ELIXIR_VERSION}/bin

# PHP Composer

RUN apt-get update && apt-get install -y php-cli php-mbstring && apt-get clean
Expand Down Expand Up @@ -143,6 +168,11 @@ RUN set -ex ;\
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain none -y ; \
rustup toolchain install 1.36.0

# Mix and Rebar

RUN mix local.hex --force
RUN mix local.rebar --force

# Pipenv

ENV PATH="/home/ubuntu/.local/bin:$PATH"
Expand Down
13 changes: 13 additions & 0 deletions docs/usage/configuration-options.md
Expand Up @@ -552,6 +552,19 @@ Set enabled to `true` to enable meteor package updating.

Add to this object if you wish to define rules that apply only to minor updates.

## mix

Elixir support is in beta stage.
It should be explicitly enabled in configuration file:

```json
{
"mix": {
"enabled": true
}
}
```

## node

Using this configuration option allows you to apply common configuration and policies across all Node.js version updates even if managed by different package managers (`npm`, `yarn`, etc.).
Expand Down
13 changes: 13 additions & 0 deletions lib/config/definitions.ts
Expand Up @@ -1421,6 +1421,19 @@ const options: RenovateOptions[] = [
},
mergeable: true,
},
{
name: 'mix',
releaseStatus: 'beta',
description: 'Configuration object for Mix module renovation',
stage: 'repository',
type: 'object',
default: {
enabled: false,
fileMatch: ['(^|/)mix\\.exs$'],
versionScheme: 'hex',
},
mergeable: true,
},
{
name: 'rust',
releaseStatus: 'unpublished',
Expand Down
97 changes: 54 additions & 43 deletions lib/datasource/hex/index.ts
Expand Up @@ -2,69 +2,80 @@ import { logger } from '../../logger';
import got from '../../util/got';
import { ReleaseResult, PkgReleaseConfig } from '../common';

function getHostOpts() {
return {
json: true,
hostType: 'hex',
};
}

interface HexRelease {
html_url: string;
meta?: { links?: Record<string, string> };
name?: string;
releases?: { version: string }[];
}

export async function getPkgReleases({
lookupName,
}: Partial<PkgReleaseConfig>): Promise<ReleaseResult | null> {
const hexUrl = `https://hex.pm/api/packages/${lookupName}`;
// istanbul ignore if
if (!lookupName) {
logger.warn('hex lookup failure: No lookupName');
return null;
}

// Get dependency name from lookupName.
// If the dependency is private lookupName contains organization name as following:
// depName:organizationName
// depName is used to pass it in hex dep url
// organizationName is used for accessing to private deps
const depName = lookupName.split(':')[0];
const hexUrl = `https://hex.pm/api/packages/${depName}`;
try {
const opts = getHostOpts();
const res: HexRelease = (await got(hexUrl, {
const response = await got(hexUrl, {
json: true,
...opts,
})).body;
if (!(res && res.releases && res.name)) {
logger.warn({ lookupName }, `Received invalid hex package data`);
hostType: 'hex',
});

const hexRelease: HexRelease = response.body;

if (!hexRelease) {
logger.warn({ depName }, `Invalid response body`);
return null;
}

const { releases = [], html_url: homepage, meta } = hexRelease;

if (releases.length === 0) {
logger.info(`No versions found for ${depName} (${hexUrl})`); // prettier-ignore
return null;
}

const result: ReleaseResult = {
releases: [],
releases: releases.map(({ version }) => ({ version })),
};
if (res.releases) {
result.releases = res.releases.map(version => ({
version: version.version,
}));

if (homepage) {
result.homepage = homepage;
}
if (res.meta && res.meta.links) {
result.sourceUrl = res.meta.links.Github;

if (meta && meta.links && meta.links.Github) {
result.sourceUrl = hexRelease.meta.links.Github;
}
result.homepage = res.html_url;

return result;
} catch (err) {
if (err.statusCode === 401) {
logger.info({ lookupName }, `Authorization failure: not authorized`);
logger.debug(
{
err,
},
'Authorization error'
);
return null;
const errorData = { depName, err };

if (
err.statusCode === 429 ||
(err.statusCode >= 500 && err.statusCode < 600)
) {
logger.warn({ lookupName, err }, `hex.pm registry failure`);
throw new Error('registry-failure');
}
if (err.statusCode === 404) {
logger.info({ lookupName }, `Dependency lookup failure: not found`);
logger.debug(
{
err,
},
'Package lookup error'
);
return null;

if (err.statusCode === 401) {
logger.debug(errorData, 'Authorization error');
} else if (err.statusCode === 404) {
logger.debug(errorData, 'Package lookup error');
} else {
logger.warn(errorData, 'hex lookup failure: Unknown error');
}
logger.warn({ err, lookupName }, 'hex lookup failure: Unknown error');
return null;
}

return null;
}
2 changes: 2 additions & 0 deletions lib/manager/index.ts
Expand Up @@ -32,6 +32,7 @@ const managerList = [
'leiningen',
'maven',
'meteor',
'mix',
'npm',
'nuget',
'nvm',
Expand All @@ -56,6 +57,7 @@ const languageList = [
'dart',
'docker',
'dotnet',
'elixir',
'golang',
'js',
'node',
Expand Down
105 changes: 105 additions & 0 deletions lib/manager/mix/artifacts.ts
@@ -0,0 +1,105 @@
import upath from 'upath';
import fs from 'fs-extra';
import { hrtime } from 'process';
import { platform } from '../../platform';
import { exec } from '../../util/exec';
import { logger } from '../../logger';
import { UpdateArtifactsConfig, UpdateArtifactsResult } from '../common';

export async function updateArtifacts(
packageFileName: string,
updatedDeps: string[],
newPackageFileContent: string,
config: UpdateArtifactsConfig
): Promise<UpdateArtifactsResult[] | null> {
await logger.debug(`mix.getArtifacts(${packageFileName})`);
if (updatedDeps.length < 1) {
logger.debug('No updated mix deps - returning null');
return null;
}

const cwd = config.localDir;
if (!cwd) {
logger.debug('No local dir specified');
return null;
}

const lockFileName = 'mix.lock';
try {
const localPackageFileName = upath.join(cwd, packageFileName);
await fs.outputFile(localPackageFileName, newPackageFileContent);
} catch (err) {
logger.warn({ err }, 'mix.exs could not be written');
return [
{
lockFileError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}

const existingLockFileContent = await platform.getFile(lockFileName);
if (!existingLockFileContent) {
logger.debug('No mix.lock found');
return null;
}

const cmdParts =
config.binarySource === 'docker'
? [
'docker',
'run',
'--rm',
`-v ${cwd}:${cwd}`,
`-w ${cwd}`,
'renovate/mix mix',
]
: ['mix'];
cmdParts.push('deps.update');

const startTime = hrtime();
/* istanbul ignore next */
try {
const command = [...cmdParts, ...updatedDeps].join(' ');
const { stdout, stderr } = await exec(command, { cwd });
logger.debug(stdout);
if (stderr) logger.warn('error: ' + stderr);
} catch (err) {
logger.warn(
{ err, message: err.message },
'Failed to update Mix lock file'
);

return [
{
lockFileError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}

const duration = hrtime(startTime);
const seconds = Math.round(duration[0] + duration[1] / 1e9);
logger.info({ seconds, type: 'mix.lock' }, 'Updated lockfile');
logger.debug('Returning updated mix.lock');

const localLockFileName = upath.join(cwd, lockFileName);
const newMixLockContent = await fs.readFile(localLockFileName, 'utf8');
if (existingLockFileContent === newMixLockContent) {
logger.debug('mix.lock is unchanged');
return null;
}

return [
{
file: {
name: lockFileName,
contents: newMixLockContent,
},
},
];
}
66 changes: 66 additions & 0 deletions lib/manager/mix/extract.ts
@@ -0,0 +1,66 @@
import { isValid } from '../../versioning/hex';
import { logger } from '../../logger';
import { PackageDependency, PackageFile } from '../common';

const depSectionRegExp = /defp\s+deps.*do/g;
const depMatchRegExp = /{:(\w+),\s*([^:"]+)?:?\s*"([^"]+)",?\s*(organization: "(.*)")?.*}/gm;

export function extractPackageFile(content: string): PackageFile {
logger.trace('mix.extractPackageFile()');
const deps: PackageDependency[] = [];
const contentArr = content.split('\n');

for (let lineNumber = 0; lineNumber < contentArr.length; lineNumber += 1) {
if (contentArr[lineNumber].match(depSectionRegExp)) {
logger.trace(`Matched dep section on line ${lineNumber}`);
let depBuffer = '';
do {
depBuffer += contentArr[lineNumber] + '\n';
lineNumber += 1;
} while (!contentArr[lineNumber].includes('end'));
let depMatch: RegExpMatchArray;
do {
depMatch = depMatchRegExp.exec(depBuffer);
if (depMatch) {
const depName = depMatch[1];
const datasource = depMatch[2];
const currentValue = depMatch[3];
const organization = depMatch[5];

const dep: PackageDependency = {
depName,
currentValue,
managerData: {},
};

dep.datasource = datasource || 'hex';

if (dep.datasource === 'hex') {
dep.currentValue = currentValue;
dep.lookupName = depName;
}

if (organization) {
dep.lookupName += ':' + organization;
}

if (!isValid(currentValue)) {
dep.skipReason = 'unsupported-version';
}

if (dep.datasource !== 'hex') {
dep.skipReason = 'non-hex depTypes';
}

// Find dep's line number
for (let i = 0; i < contentArr.length; i += 1)
if (contentArr[i].includes(`:${depName},`))
dep.managerData.lineNumber = i;

deps.push(dep);
}
} while (depMatch);
}
}
return { deps };
}

0 comments on commit 982896d

Please sign in to comment.