Skip to content

Commit

Permalink
Implement c_cpp_properties.json file generator fixes #1201 (#1205)
Browse files Browse the repository at this point in the history
* Implement cpp properties file generator

* Run configure and cleanup scripts

* Fix linux default c std

* Fix typo

* Seperate LinkingTo extraction to R file

* Clarify extractLinkingTo.R

* CamelCase cppProperties folder

* Add more checks

* Transfer linking to paths URL encoded

* Minor trim

* Rename generate c/cpp config command

Co-authored-by: Kun Ren <renkun@outlook.com>
  • Loading branch information
nx10 and renkun-ken authored Sep 30, 2022
1 parent e3ede0d commit ad20fdc
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 0 deletions.
13 changes: 13 additions & 0 deletions R/cppProperties/extractLinkingTo.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
deps <- read.dcf("DESCRIPTION", "LinkingTo")
if (length(deps) == 0) { # Empty file
deps <- ""
} else {
deps <- unname(deps[1, ]) # Read 'LinkingTo' field from description
deps <- gsub("\\s|\\n|(\\([^\\)]*\\))", "", deps) # Remove all whitespace, line breaks and version constraints
deps <- strsplit(deps, ",")[[1]] # Split package names
deps <- vapply(deps, function(pkg) {
system.file("include", package = pkg)
}, character(1)) # Lookup include dir
deps <- utils::URLencode(deps)
}
writeLines(deps)
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"onCommand:r.goToNextChunk",
"onCommand:r.createGitignore",
"onCommand:r.createLintrConfig",
"onCommand:r.generateCCppProperties",
"onCommand:r.runCommandWithSelectionOrWord",
"onCommand:r.runCommandWithEditorPath",
"onCommand:r.runCommand",
Expand Down Expand Up @@ -475,6 +476,11 @@
"category": "R Package",
"command": "r.test"
},
{
"title": "Generate C/C++ Configuration",
"category": "R Package",
"command": "r.generateCCppProperties"
},
{
"title": "Attach Active Terminal",
"category": "R",
Expand Down
214 changes: 214 additions & 0 deletions src/cppProperties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
'use strict';

import { randomBytes } from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { window } from 'vscode';
import { getRpath, getCurrentWorkspaceFolder, executeRCommand } from './util';
import { execSync } from 'child_process';
import { extensionContext } from './extension';

export async function generateCppProperties(): Promise<void> {
const currentWorkspaceFolder = getCurrentWorkspaceFolder()?.uri.fsPath;
if (currentWorkspaceFolder === undefined) {
void window.showWarningMessage('Please open a workspace folder to create c_cpp_properties.json');
return;
}
const outFilePath = path.join(currentWorkspaceFolder, '.vscode', 'c_cpp_properties.json');
if (fs.existsSync(outFilePath)) {
const overwrite = await window.showWarningMessage(
'"c_cpp_properties.json" file already exists. Do you want to overwrite?',
'Yes', 'No'
);
if (overwrite === 'No') {
return;
}
void fs.unlinkSync(outFilePath);
}
return generateCppPropertiesProc(currentWorkspaceFolder);
}

/** Helper: Return object depending on current process platform */
function platformChoose<A, B, C>(win32: A, darwin: B, other: C): A | B | C {
return process.platform === 'win32' ? win32 :
process.platform === 'darwin' ? darwin :
other;
}

// See: https://code.visualstudio.com/docs/cpp/c-cpp-properties-schema-reference
async function generateCppPropertiesProc(workspaceFolder: string) {
const rPath = await getRpath();

// Collect information from running the compiler
const configureFile = platformChoose('configure.win', 'configure', 'configure');
const cleanupFile = platformChoose('cleanup.win', 'cleanup', 'cleanup');

if (fs.existsSync(path.join(workspaceFolder, configureFile))) {
await executeRCommand(`system("sh ./${configureFile}")`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
});
}

const compileOutputCpp = collectCompilerOutput(rPath, workspaceFolder, 'cpp');
const compileOutputC = collectCompilerOutput(rPath, workspaceFolder, 'c');

if (fs.existsSync(path.join(workspaceFolder, cleanupFile))) {
await executeRCommand(`system("sh ./${cleanupFile}")`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
});
}

const compileInfo = extractCompilerInfo(compileOutputCpp);
const compileStdCpp = extractCompilerStd(compileOutputCpp);
const compileStdC = extractCompilerStd(compileOutputC);
const compileCall = extractCompilerCall(compileOutputCpp);
const compilerPath = await executeRCommand(`cat(Sys.which("${compileCall}"))`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
});

const intelliSensePlatform = platformChoose('windows', 'macos', 'linux');
const intelliSenseComp = compileCall ? (compileCall.includes('clang') ? 'clang' : 'gcc') : 'gcc';
const intelliSense = `${intelliSensePlatform}-${intelliSenseComp}-${process.arch}`;

// Collect information from 'DESCRIPTION'
const linkingToIncludes = await collectRLinkingTo(workspaceFolder);

// Combine information
const envIncludes: string[] = ['${workspaceFolder}/src'];
envIncludes.push(...compileInfo.compIncludes.map((v) => path.isAbsolute(v) ? v : `\${workspaceFolder}/${path.join('src', v)}`));
envIncludes.push(...linkingToIncludes);

const envDefines = compileInfo.compDefines;

// If no standard is set on linux, the C standard seems to default to the c++ one.
const envCStd = (!compileStdC || compileStdC.includes('++')) ? '${default}' : compileStdC;

const platformName = platformChoose('Win32', 'Mac', 'Linux');

// Build json
const re = {
'configurations': [{
'name': platformName,
'defines': envDefines,
'includePath': envIncludes,
'compilerPath': compilerPath,
'cStandard': envCStd,
'cppStandard': compileStdCpp,
'intelliSenseMode': intelliSense
}],
'version': 4
};
const ser = JSON.stringify(re, null, 2);

// Write file
const vscodeDir = path.join(workspaceFolder, '.vscode');
if (!fs.existsSync(vscodeDir)) {
fs.mkdirSync(vscodeDir);
}
fs.writeFileSync(path.join(vscodeDir, 'c_cpp_properties.json'), ser);
}

async function collectRLinkingTo(workspaceFolder: string): Promise<string[]> {
if (!fs.existsSync(path.join(workspaceFolder, 'DESCRIPTION'))) {
return [];
}

const rScript = extensionContext.asAbsolutePath('R/cppProperties/extractLinkingTo.R').replace(/\\/g, '/');
const linkingToIncludesStr = (await executeRCommand(`source('${rScript}')`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
}))?.trim();
if (!linkingToIncludesStr || linkingToIncludesStr === '') {
return [];
}
return linkingToIncludesStr.split(/\r?\n/g).map(decodeURI);
}

function ensureUnquoted(str: string): string {
if (/(^".*"$)|(^'.*'$)/.test(str)) {
return str.substring(1, str.length - 1);
}
return str;
}

function extractCompilerInfo(compileOutput: string) {
const rxCompArg = /-(I|D)("[^"]+"|[\S]+)/gm;

const compDefines: string[] = [];
const compIncludes: string[] = [];
const compLookup = { 'D': compDefines, 'I': compIncludes };

let m: RegExpExecArray | null;
while ((m = rxCompArg.exec(compileOutput)) !== null) {
if (m.index === rxCompArg.lastIndex) {
rxCompArg.lastIndex++;
}

// The regex guarantees that the first group is 'I' or 'D'
compLookup[(m[1] as 'D' | 'I')].push(ensureUnquoted(m[2]));
}

return {
compDefines: compDefines,
compIncludes: compIncludes
};
}

function extractCompilerStd(compileOutput: string): string | undefined {
const rxStd = /-std=(\S+)/;

const stdMatch = compileOutput.match(rxStd);
return stdMatch?.[1];
}

function extractCompilerCall(compileOutput: string): string | undefined {
const rxComp = /("[^"]+"|[\S]+)/;
const ccalls = compileOutput.split('\n');
if (ccalls.length < 2) {
return undefined;
}

const m = ccalls[1].match(rxComp);
return m?.[1];
}

function createTempDir(root: string): string {
let tempDir: string;
while (fs.existsSync(tempDir = path.join(root, `___temp_${randomBytes(8).toString('hex')}`))) { /* Name clash */ }
fs.mkdirSync(tempDir);
return tempDir;
}

function collectCompilerOutput(rPath: string, workspaceFolder: string, testExtension: 'cpp' | 'c') {

const makevarsFiles = ['Makevars', 'Makevars.win', 'Makevars.ucrt'];

const srcFolder = path.join(workspaceFolder, 'src');
const tempFolder = createTempDir(workspaceFolder);

// Copy makevars
if (fs.existsSync(srcFolder)) {
const projectMakevarsFiles = fs.readdirSync(srcFolder).filter(fn => makevarsFiles.includes(fn));
for (const f of projectMakevarsFiles) {
fs.copyFileSync(path.join(srcFolder, f), path.join(tempFolder, f));
}
}

// Create dummy source file
const testFile = `comp_test.${testExtension}`;
fs.writeFileSync(path.join(tempFolder, testFile), '');

// Compile dummy
const command = `"${rPath}" CMD SHLIB ${testFile}`;
const compileOutput = execSync(command, {
cwd: tempFolder
}).toString();

// Cleanup
fs.rmSync(tempFolder, { recursive: true, force: true });

return compileOutput;
}
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path = require('path');
import * as preview from './preview';
import * as rGitignore from './rGitignore';
import * as lintrConfig from './lintrConfig';
import * as cppProperties from './cppProperties';
import * as rTerminal from './rTerminal';
import * as session from './session';
import * as util from './util';
Expand Down Expand Up @@ -113,6 +114,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
// editor independent commands
'r.createGitignore': rGitignore.createGitignore,
'r.createLintrConfig': lintrConfig.createLintrConfig,
'r.generateCCppProperties': cppProperties.generateCppProperties,
'r.loadAll': () => rTerminal.runTextInTerm('devtools::load_all()'),

// environment independent commands. this is a workaround for using the Tasks API: /~https://github.com/microsoft/vscode/issues/40758
Expand Down

0 comments on commit ad20fdc

Please sign in to comment.