2
0
mirror of https://github.com/boostorg/url.git synced 2026-01-26 19:12:14 +00:00
Files
url/doc/generate-files.mjs
2024-07-11 03:21:49 -03:00

341 lines
13 KiB
JavaScript

#!/usr/bin/env node
//
// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/url
//
'use strict';
import {execSync} from 'child_process'
import fs from 'fs'
import path from 'path'
import os from 'os'
import request from 'sync-request'
/*
Helper functions
*/
function mkdirp(dir) {
if (!fs.existsSync(dir)) {
mkdirp(path.dirname(dir))
fs.mkdirSync(dir)
}
}
function findExecutable(executableName) {
if (Array.isArray(executableName)) {
for (const name of executableName) {
const result = findExecutable(name)
if (result) {
return result
}
}
return undefined
}
const isWin = process.platform === 'win32';
const pathDirs = process.env.PATH.split(isWin ? ';' : ':');
const extensions = isWin ? ['.exe', '.bat', '.cmd'] : [''];
function isExecutable(filePath) {
try {
if (!isWin) {
fs.accessSync(filePath, fs.constants.X_OK);
}
return true;
} catch (error) {
return false;
}
}
// Try to find the exact executable first
for (const dir of pathDirs) {
for (const ext of extensions) {
const fullPath = path.join(dir, executableName + ext);
if (fs.existsSync(fullPath) && isExecutable(fullPath)) {
return fullPath;
}
}
}
function escapeRegExp(string) {
// Escape special characters for use in regex
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// If the exact executable is not found, search for versioned executables
const versionedExecutables = [];
const escapedExecutableName = escapeRegExp(executableName);
const versionRegex = new RegExp(`${escapedExecutableName}-(\\d+)$`);
for (const dir of pathDirs) {
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (!extensions.some(ext => file.endsWith(ext))) {
continue
}
const fullPath = path.join(dir, file);
if (!isExecutable(fullPath)) {
continue
}
const ext = path.extname(file);
const basename = path.basename(file, ext);
const match = basename.match(versionRegex);
if (match) {
versionedExecutables.push({
path: fullPath,
version: parseInt(match[1], 10)
});
}
}
} catch (error) {
// Ignore errors from reading directories
}
}
if (versionedExecutables.length > 0) {
versionedExecutables.sort((a, b) => b.version - a.version);
return versionedExecutables[0].path;
}
return undefined;
}
function mkTmpDir() {
let tempDir = undefined
try {
tempDir = fs.mkdtempSync(os.tmpdir())
} catch (error) {
console.error(`Could not create temporary directory with fs.mkdtempSync(os.tmpdir()): ${error}.`)
try {
console.error(`Trying to create temporary directory in fs.mkdtempSync('~/tmp')...`)
tempDir = fs.mkdtempSync(path.join(os.homedir(), 'tmp'));
} catch (error) {
console.error(`Could not create temporary directory with fs.mkdtempSync('~/tmp'): ${error}.`)
try {
console.error(`Trying to create temporary directory in fs.mkdtempSync(path.join(cwd, 'tmp'))...`)
tempDir = fs.mkdtempSync(path.join(cwd, 'tmp'));
console.log(`Created temporary directory ${tempDir}`)
} catch (error) {
console.error(`Could not create temporary directory: ${error}`)
}
}
}
return tempDir
}
function downloadAndDecompress(downloadUrl, downloadPath, extractPath) {
try {
console.log(`Downloading ${downloadUrl} to ${downloadPath}...`);
const response = request('GET', downloadUrl)
fs.writeFileSync(downloadPath, response.getBody());
console.log(`File ${downloadUrl} downloaded successfully to ${downloadPath}.`);
if (path.extname(downloadPath) === '.7z') {
const cmd = `7z x ${downloadPath} -o${extractPath}`
console.log(`Decompressing with command: ${cmd}`)
execSync(cmd, {stdio: 'inherit'})
} else if (/\.tar\.gz$/.test(downloadPath)) {
mkdirp(extractPath)
const cmd = `tar -vxzf ${downloadPath} -C ${extractPath}`
console.log(`Decompressing with command: ${cmd}`)
execSync(cmd, {stdio: 'inherit'})
}
console.log(`File decompressed successfully to ${extractPath}.`);
} catch (error) {
console.error('Error:', error.message);
}
}
/*
Find C++ compilers
Clang++ is preferred over g++ and cl for MrDocs
*/
const cwd = process.cwd()
let cxxCompiler = findExecutable(['clang++', 'g++', 'cl']) || process.env.CXX_COMPILER || process.env.CXX
if (cxxCompiler && process.platform === "win32") {
// Replace "\" with "/" in CXX_COMPILER
cxxCompiler = cxxCompiler.replace(/\\/g, '/')
}
if (cxxCompiler === undefined) {
console.error('Could not find a C++ compiler. Please set the CXX_COMPILER environment variable.')
process.exit(1)
}
process.env.CMAKE_CXX_COMPILER = cxxCompiler
process.env.CXX = cxxCompiler
const cxxCompilerName = path.basename(cxxCompiler).replace(/\.exe$/, '')
let cCompiler = findExecutable(['clang', 'gcc', 'cl']) || process.env.C_COMPILER || process.env.CC
if (cCompiler && process.platform === "win32") {
// Replace "\" with "/" in CXX_COMPILER
cCompiler = cCompiler.replace(/\\/g, '/')
}
if (cCompiler === undefined) {
console.error('Could not find a C compiler. Please set the C_COMPILER environment variable.')
process.exit(1)
}
const cCompilerName = path.basename(cCompiler).replace(/\.exe$/, '')
process.env.CMAKE_C_COMPILER = cCompiler
process.env.CC = cCompiler
console.log(`C++ compiler: ${cxxCompilerName} (${cxxCompiler})`)
console.log(`C compiler: ${cCompilerName} (${cCompiler})`)
/*
Download Boost
*/
function isBoostDir(dir) {
if (!fs.existsSync(dir)) {
return false
}
console.log(`Checking if ${dir} is a Boost source directory`)
const boostFiles = ['CMakeLists.txt', 'Jamroot', 'boost-build.jam', 'bootstrap.sh', 'libs']
for (const file of boostFiles) {
const filePath = path.join(cwdParentParent, file)
console.log(`Checking if ${filePath} exists`)
if (!fs.existsSync(filePath)) {
return false
}
}
return true
}
// Check if we need to download boost. This is usually the case in CI, but we want to reuse the local boost directory
// in local builds.
const librariesJsonPath = path.join(cwd, 'meta', 'libraries.json')
const librariesJson = JSON.parse(fs.readFileSync(librariesJsonPath, 'utf-8'))
const module = librariesJson['key']
const self = module || path.basename(cwd)
console.log(`self: ${self}`)
// Check if cwdParentParent contains the files "CMakeLists.txt" "Jamroot" "boost-build.jam" "bootstrap.sh" "libs"
let tempDir = undefined
let boostDir = undefined
const cwdParentParent = path.dirname(path.dirname(cwd))
if (isBoostDir(cwdParentParent)) {
boostDir = cwdParentParent
console.log(`Found Boost at ${cwdParentParent}`)
} else {
const branch = execSync('git rev-parse --abbrev-ref HEAD', {encoding: 'utf-8'}).trim()
const boostBranchRegex = /^boost-1\.\d+\.\d+$/
const isValidBoostBranch = branch === 'master' || branch === 'develop' || boostBranchRegex.test(branch);
const boostBranch = isValidBoostBranch ? branch : 'develop'
// Clone boost in a temporary directory
if (!tempDir) {
tempDir = mkTmpDir()
}
console.log(`Created temporary directory ${tempDir}`)
let boostDownloadCmd = `git clone https://github.com/boostorg/boost.git --depth 1 --branch ${boostBranch}`
execSync(boostDownloadCmd, {cwd: tempDir})
boostDir = path.join(tempDir, 'boost')
console.log(`Initializing submodules in ${boostDir}`)
execSync('git submodule update --init --recursive', {cwd: boostDir})
console.log(`Cloned Boost to ${boostDir}`)
// Delete `self` from boost/libs
const selfDir = path.join(boostDir, 'libs', self)
console.log(`Deleting ${selfDir}`)
execSync(`rm -rf ${selfDir}`)
// Copy contents of cwd to boost/libs/self
// const selfDirParent = path.join(boostDir, 'libs')
console.log(`Copying ${cwd} to ${selfDir}`)
execSync(`cp -r ${cwd} ${selfDir}`)
}
/*
Find and run MrDocs to generate the documentation
Install MrDocs with:
- Linux:
wget https://github.com/cppalliance/mrdocs/releases/download/develop-release/MrDocs-0.0.1-Linux.tar.gz
sudo tar -xzf MrDocs-0.0.1-Linux.tar.gz -C /usr/local --strip-components=1
- Windows (Powershell):
Invoke-WebRequest -Uri 'https://github.com/cppalliance/mrdocs/releases/download/develop-release/MrDocs-0.0.1-win64.7z' -OutFile 'MrDocs-0.0.1-win64.7z'
7z x -o"C:\Users\$env:USERNAME\Applications" 'MrDocs-0.0.1-win64.7z'
( Adapt the destination path as needed )
*/
// https://github.com/terascope/fetch-github-release
let mrDocsExec = findExecutable('mrdocs')
if (!mrDocsExec) {
console.log(`Could not find MrDocs. Downloading...`)
if (!tempDir) {
tempDir = mkTmpDir()
}
const releasesResponse = request('GET', 'https://api.github.com/repos/cppalliance/mrdocs/releases', {
headers: {
'User-Agent': 'request'
}
})
const releasesInfo = JSON.parse(releasesResponse.getBody('utf-8'))
console.log(`Found ${releasesInfo.length} MrDocs releases`)
let downloadUrl = undefined
for (const latestRelease of releasesInfo) {
console.log(`Latest release: ${latestRelease['tag_name']}`)
const latestAssets = latestRelease['assets'].map(asset => asset['browser_download_url'])
console.log(`Latest assets: ${latestAssets}`)
downloadUrl = process.platform === "win32" ? latestAssets.find(asset => asset.endsWith('win64.7z')) : latestAssets.find(asset => asset.endsWith('Linux.tar.gz'))
if (downloadUrl) {
break
}
console.warn(`Could not find MrDocs binaries in ${latestRelease['tag_name']} release for ${process.platform}`)
}
if (!downloadUrl) {
console.error(`Could not find MrDocs binaries for ${process.platform}`)
process.exit(1)
}
const downloadFilename = path.basename(downloadUrl)
console.log(`Downloading ${downloadUrl} to ${path.join(tempDir, downloadFilename)}...`)
downloadAndDecompress(
downloadUrl,
path.join(tempDir, downloadFilename),
path.join(tempDir, 'MrDocs')
)
console.log(`Extracted ${downloadFilename} to ${path.join(tempDir, 'MrDocs')}`)
const downloadExtractDir = downloadFilename.replace(/\.(7z|tar\.gz)$/, '')
const downloadExtractBinFilename = process.platform === "win32" ? 'mrdocs.exe' : 'mrdocs'
mrDocsExec = path.join(tempDir, 'MrDocs', downloadExtractDir, 'bin', downloadExtractBinFilename)
if (!fs.existsSync(mrDocsExec)) {
console.error(`Could not find MrDocs at ${mrDocsExec}`)
process.exit(1)
} else {
console.log(`Found MrDocs executable at ${mrDocsExec}`)
}
}
console.log(`Found MrDocs at ${mrDocsExec}`)
// Reference goes to another module so that its relative links work
// Antora does not support relative xrefs: https://gitlab.com/antora/antora/-/issues/428
const buildDirectory = path.join(cwd, 'doc', 'build')
const mrDocsOutputDir = path.join(buildDirectory, 'generated-files', 'modules', 'reference', 'pages')
const mrDocsProjectPath = path.join(boostDir, 'libs', 'url')
const mrDocsConfigPath = path.join(mrDocsProjectPath, 'doc', 'mrdocs.yml')
const mrDocsCmd = `${mrDocsExec} --config="${mrDocsConfigPath}" "${mrDocsProjectPath}" --output="${mrDocsOutputDir}"`
console.log(`Generating documentation with command: ${mrDocsCmd}`)
// Execute mrDocsCmd with execSync, get output and print it to stdout. Also get return code.
// If return code is not 0, print output to stderr and exit with return code.
let mrDocsExitCode = 0
try {
const mrDocsOutput = execSync(mrDocsCmd)
console.log(mrDocsOutput.toString())
console.log(`Generated documentation at ${mrDocsOutputDir}`)
} catch (error) {
console.error('Failed to run MrDocs')
console.error(error.stdout.toString())
mrDocsExitCode = error.status
}
if (tempDir) {
console.log(`Deleting temporary directory ${tempDir}`)
execSync(`rm -rf ${tempDir}`)
}
if (mrDocsExitCode !== 0) {
console.log(`Exiting with code ${mrDocsExitCode}`)
}
process.exit(mrDocsExitCode)