Browse Source

New RESTful API

Partially addresses #179 and begins opening the door to #203.
dev/git-series/gccdum
Matt Godbolt 6 years ago
parent
commit
23efc05167
  1. 56
      README.md
  2. 59
      app.js
  3. 4
      etc/config/c++.lud-mgodbolt01.properties
  4. 12
      lib/base-compiler.js
  5. 62
      lib/compile-handler.js
  6. 9
      lib/utils.js
  7. 1
      package.json
  8. 2
      static/compiler.js

56
README.md

@ -37,3 +37,59 @@ Feel free to raise an issue on [github](https://github.com/mattgodbolt/compiler-
Compiler Explorer is maintained by [Matt Godbolt](http://xania.org). Multiple compiler and difference view was
implemented by [Gabriel Devillers](https://github.com/voxelf).
### RESTful API
There's a simple restful API that can be used to do compiles to asm and to list compilers. In general
all handlers live in `/api/*` endpoints, and will accept JSON or text in POSTs, and will return text responses
or JSON responses depending on the request's `Accept` header.
At a later date there may be some form of rate-limiting: currently requests will be queued and dealt with
exactly like interactive requests on the main site. Authentication might be required at some point in the
future (for the main Compiler Explorer site anyway).
The following endpoints are defined:
#### `GET /api/compilers` - return a list of compilers
Returns a list of compilers. In text form, there's a simple formatting of the ID of the compiler and its
description. In JSON, all the information is returned as an array of compilers, with the `id` key being the
primary identifier of each compiler.
#### `POST /api/<compiler-id>/compiler` - perform a compilation
To specify a compilation request as a JSON document, post it as the appropriate type and send an object of
the form: `{'source': 'source to compile', 'options': 'compiler flags', 'filters': {'filter': true}}`. The filters are an JSON object with true/false. If not supplied, defaults are used. If supplied, the filters are used
as-is.
A text compilation request has the source as the body of the post, and uses query parameters to pass the
options and filters. Filters are supplied as a comma-separated string. Use the query parameter `filters=XX`
to set the filters directly, else `addFilters=XX` to add a filter to defaults, or `removeFilters` to remove from defaults. Compiler parameters should be passed as `options=-O2` and default to empty.
Filters include `binary`, `labels`, `intel`, `comments` and `directives` and correspond to the UI buttons on
the HTML version.
The text request is designed for simplicity for command-line clients like `curl`:
```bash
$ curl 'https://gcc.godbolt.org/api/compiler/g63/compile?options=-Wall' --data-binary 'int foo() { return 1; }'
# Compilation provided by Compiler Explorer at gcc.godbolt.org
foo():
push rbp
mov rbp, rsp
mov eax, 1
pop rbp
ret
```
If JSON is present in the request's `Accept` header, the compilation results are of the form:
```json
{
code: 0 if successful, else compiler return code,
stdout: [ { text: "Output",
(optional) tag: {line: source line, text: "parsed error for that line"} } ],
stderr: (as above),
asm: [ { text: "assembly text", source: source line number or null if none } ]
}
```

59
app.js

@ -138,8 +138,8 @@ fileSources.forEach(function (source) {
});
var clientOptionsHandler = new ClientOptionsHandler(fileSources);
var apiHandler = new ApiHandler();
var compileHandler = new CompileHandler(gccProps, compilerProps);
var apiHandler = new ApiHandler(compileHandler);
// auxiliary function used in clientOptionsHandler
function compareOn(key) {
@ -386,27 +386,32 @@ function findCompilers() {
});
}
// Instantiate a function that writes information on the compiler,
// in JSON format, for ease of external listing.
function ApiHandler() {
var reply = "";
function ApiHandler(compileHandler) {
this.compilers = [];
this.compileHandler = compileHandler;
this.setCompilers = function (compilers) {
reply = JSON.stringify(compilers);
this.compilers = compilers;
};
this.handler = function apiHandler(req, res, next) {
var bits = req.url.split("/");
if (bits.length !== 2 || req.method !== "GET") return next();
switch (bits[1]) {
default:
next();
break;
case "compilers":
res.set('Content-Type', 'application/json');
res.end(reply);
break;
this.handler = express.Router();
this.handler.get('/compilers', _.bind(function (req, res, next) {
if (req.accepts(['text', 'json']) == 'json') {
res.set('Content-Type', 'application/json');
res.end(JSON.stringify(this.compilers));
} else {
res.set('Content-Type', 'text/plain');
var title = 'Compiler Name';
var maxLength = _.max(_.pluck(_.pluck(this.compilers, 'id').concat([title]), 'length'));
res.write(padRight(title, maxLength) + ' | Description\n');
res.end(_.map(this.compilers, function (compiler) {
return padRight(compiler.id, maxLength) + ' | ' + compiler.name + '\n';
}).join("\n"));
}
};
}, this));
this.handler.param('compiler', _.bind(function (req, res, next, compilerName) {
req.compiler = compilerName;
next();
}, this));
this.handler.post('/compiler/:compiler/compile', this.compileHandler.handler);
}
function shortUrlHandler(req, res, next) {
@ -484,7 +489,6 @@ findCompilers()
var webServer = express(),
sFavicon = require('serve-favicon'),
sStatic = require('serve-static'),
bodyParser = require('body-parser'),
morgan = require('morgan'),
compression = require('compression'),
@ -501,24 +505,29 @@ findCompilers()
.use(morgan('combined', {stream: logger.stream}))
.use(compression())
.use(sFavicon(staticDir + '/favicon.ico'))
.use('/v', sStatic(staticDir + '/v', {maxAge: Infinity}))
.use(sStatic(staticDir, {maxAge: staticMaxAgeSecs * 1000}));
.use('/v', express.static(staticDir + '/v', {maxAge: Infinity, index: false}))
.use(express.static(staticDir, {maxAge: staticMaxAgeSecs * 1000}));
if (archivedVersions) {
// The archived versions directory is used to serve "old" versioned data during updates. It's expected
// to contain all the SHA-hashed directories from previous versions of Compiler Explorer.
logger.info(" serving archived versions from", archivedVersions);
webServer.use('/v', sStatic(archivedVersions, {maxAge: Infinity}));
webServer.use('/v', express.static(archivedVersions, {maxAge: Infinity, index: false}));
}
webServer
.use(bodyParser.json({limit: gccProps('bodyParserLimit', '1mb')}))
.use(bodyParser.text({
limit: gccProps('bodyParserLimit', '1mb'), type: function () {
return true;
}
}))
.use(restreamer())
.get('/client-options.json', clientOptionsHandler.handler)
.use('/source', getSource)
.use('/api', apiHandler.handler)
.use('/g', shortUrlHandler)
.use('/e', embeddedHandler)
.post('/compile', compileHandler.handler) // used inside static/compiler.js
.post('/diff', diffHandler); // used inside static/compiler.js
.post('/compile', compileHandler.handler)
.post('/diff', diffHandler);
logger.info("=======================================");
webServer.on('error', function (err) {

4
etc/config/c++.lud-mgodbolt01.properties

@ -1,7 +1,7 @@
# Default settings for GCC Explorer.
defaultCompiler=g52
#compilers=g44:g45:g46:clang35:g51:g52:gdef:msp430g453:cl
compilers=&gcc:&clang:&windows
compilers=g44:g45:g46:clang35:g51:g52:gdef:msp430g453:cl
#compilers=&gcc:&clang:&windows
group.gcc.compilers=gdef:g62
compiler.gdef.exe=/usr/bin/g++
compiler.gdef.name=Default G++

12
lib/base-compiler.js

@ -271,9 +271,11 @@ Compile.prototype.exec = function (command, args, options) {
detached: process.platform == 'linux'
});
var running = true;
function kill() {
if (running) treeKill(child.pid);
}
var stderr = "";
var stdout = "";
var timeout;
@ -387,4 +389,14 @@ Compile.prototype.getInfo = function () {
return this.compiler;
};
Compile.prototype.getDefaultFilters = function () {
// TODO; propagate to UI?
return {
intel: true,
commentOnly: true,
directives: true,
labels: true
};
};
module.exports = Compile;

62
lib/compile-handler.js

@ -85,9 +85,37 @@ function CompileHandler(gccProps, compilerProps) {
var proxy = httpProxy.createProxyServer({});
this.handler = _.bind(function compile(req, res, next) {
var compiler = this.compilersById[req.body.compiler];
if (!compiler) return next();
var source, options, filters, compiler;
if (req.is('json')) {
// JSON-style request
compiler = this.compilersById[req.compiler || req.body.compiler];
if (!compiler) return next();
source = req.body.source;
options = req.body.options;
filters = req.body.filters || compiler.getDefaultFilters();
} else {
// API-style
compiler = this.compilersById[req.compiler];
if (!compiler) return next();
source = req.body;
options = req.query.options;
// By default we get the default filters.
filters = compiler.getDefaultFilters();
// If specified exactly, we'll take that with ?filters=a,b,c
if (req.query.filters) {
filters = _.object(_.map(req.query.filters.split(","), function (filter) {
return [filter, true];
}));
}
// Add a filter. ?addFilters=binary
_.each((req.query.addFilters || "").split(","), function (filter) {
filters[filter] = true;
});
// Remove a filter. ?removeFilter=intel
_.each((req.query.removeFilters || "").split(","), function (filter) {
delete filters[filter];
});
}
var remote = compiler.getRemote();
if (remote) {
proxy.web(req, res, {target: remote}, function (e) {
@ -96,23 +124,39 @@ function CompileHandler(gccProps, compilerProps) {
});
return;
}
var source = req.body.source;
var options = req.body.options || '';
if (source === undefined) {
return next(new Error("Bad request"));
}
options = _.chain(quote.parse(options)
options = _.chain(quote.parse(options || '')
.map(function (x) {
if (typeof(x) == "string") return x;
return x.pattern;
}))
.filter(_.identity)
.value();
var filters = req.body.filters;
function textify(array) {
return _.pluck(array || [], 'text').join("\n");
}
compiler.compile(source, options, filters).then(
function (result) {
res.set('Content-Type', 'application/json');
res.end(JSON.stringify(result));
if (req.accepts(['text', 'json']) === 'json') {
res.set('Content-Type', 'application/json');
res.end(JSON.stringify(result));
} else {
res.set('Content-Type', 'text/plain');
try {
res.write("# Compilation provided by Compiler Explorer at " + req.get('Host') + "\n");
res.write(textify(result.asm));
if (result.code !== 0) res.write("\n# Compiler exited with result code " + result.code);
if (!_.isEmpty(result.stdout)) res.write("\nStandard out:\n" + textify(result.stdout));
if (!_.isEmpty(result.stderr)) res.write("\nStandard error:\n" + textify(result.stderr));
} catch (ex) {
re.write("Error handling request: " + ex);
}
res.end('\n');
}
},
function (error) {
logger.error("Error: " + error);

9
lib/utils.js

@ -66,4 +66,11 @@ function parseOutput(lines, inputFilename) {
return result;
}
exports.parseOutput = parseOutput;
exports.parseOutput = parseOutput;
function padRight(name, len) {
while (name.length < len) name = name + ' ';
return name;
}
exports.padRight = padRight;

1
package.json

@ -28,7 +28,6 @@
"promise-queue": "2.1.x",
"nopt": "3.0.x",
"serve-favicon": "2.3.x",
"serve-static": "1.10.x",
"temp": "0.8.x",
"tree-kill": "1.1.x",
"underscore-node": "*",

2
static/compiler.js

@ -214,7 +214,7 @@ define(function (require) {
}, this), 500);
$.ajax({
type: 'POST',
url: '/compile',
url: '/api/compiler/' + request.compiler + '/compile',
dataType: 'json',
contentType: 'application/json',
data: jsonRequest,

Loading…
Cancel
Save