diff --git a/config/build.conf.js b/config/build.conf.js index 66e52e09fe..601ba1a0f9 100644 --- a/config/build.conf.js +++ b/config/build.conf.js @@ -8,6 +8,11 @@ const os = require('os'); */ const ROOT = path.join(__dirname, '..'); +/** + * The tmp folder location. + */ + const TMP_DIR = path.join(ROOT, 'tmp'); + /** * The mongosh package. */ @@ -44,7 +49,7 @@ const EXECUTABLE_PATH = path.join(OUTPUT_DIR, process.platform === 'win32' ? 'mo * We use the name mongocryptd-mongosh to avoid conflicts with users * potentially installing the 'proper' mongocryptd package. */ -const MONGOCRYPTD_PATH = path.resolve(__dirname, '..', 'tmp', 'mongocryptd-mongosh' + (process.platform === 'win32' ? '.exe' : '')); +const MONGOCRYPTD_PATH = path.resolve(TMP_DIR, 'mongocryptd-mongosh' + (process.platform === 'win32' ? '.exe' : '')); /** * Build info JSON data file. @@ -66,6 +71,11 @@ const REVISION = process.env.GITHUB_COMMIT ?? process.env.REVISION; */ const COPYRIGHT = `${new Date().getYear() + 1900} MongoDB, Inc.`; +/** + * The manual page file name + */ +const MANPAGE_NAME = 'mongosh.1.gz' + /** * Export the configuration for the build. */ @@ -141,6 +151,10 @@ module.exports = { packagedFilePath: 'THIRD_PARTY_NOTICES' } ], + manpage: { + sourceFilePath: path.resolve(TMP_DIR, 'manpage', MANPAGE_NAME), + packagedFilePath: MANPAGE_NAME, + }, metadata: { name: 'mongosh', rpmName: 'mongodb-mongosh', @@ -159,5 +173,10 @@ module.exports = { debTemplateDir: path.resolve(__dirname, '..', 'packaging', 'deb-template'), rpmTemplateDir: path.resolve(__dirname, '..', 'packaging', 'rpm-template'), msiTemplateDir: path.resolve(__dirname, '..', 'packaging', 'msi-template') - } + }, + manpage: { + sourceUrl: 'https://docs.mongodb.com/mongodb-shell/manpages.tar.gz', + downloadPath: path.resolve(TMP_DIR, 'manpage'), + fileName: MANPAGE_NAME, + }, }; diff --git a/packages/build/src/config/config.ts b/packages/build/src/config/config.ts index d4f3fea76c..5327e5bed2 100644 --- a/packages/build/src/config/config.ts +++ b/packages/build/src/config/config.ts @@ -1,6 +1,12 @@ import type { PackageInformation } from '../packaging/package'; import { BuildVariant } from './build-variant'; +interface ManPageConfig { + sourceUrl: string; + downloadPath: string; + fileName: string; +} + /** * Defines the configuration interface for the build system. */ @@ -42,4 +48,5 @@ export interface Config { mongocryptdPath: string; packageInformation?: PackageInformation; artifactUrlFile?: string; + manpage?: ManPageConfig; } diff --git a/packages/build/src/packaging/download-manpage.spec.ts b/packages/build/src/packaging/download-manpage.spec.ts new file mode 100644 index 0000000000..913d675fae --- /dev/null +++ b/packages/build/src/packaging/download-manpage.spec.ts @@ -0,0 +1,20 @@ +import nock from 'nock'; +import { join } from 'path'; +import { promises as fs } from 'fs'; +import { downloadManpage } from './download-manpage'; + +describe('packaging download manpage', () => { + it('downloads manpage', async() => { + nock('http://example.com') + .get('/') + .replyWithFile( + 200, + join(__dirname, '..', '..', 'test', 'fixtures', 'manpages.tar.gz') + ); + + const destination = join(__dirname, '..', '..', 'tmp', 'manpage'); + const name = 'manpages.gz'; + await downloadManpage('http://example.com', destination, name); + await fs.access(join(destination, name)); + }); +}); diff --git a/packages/build/src/packaging/download-manpage.ts b/packages/build/src/packaging/download-manpage.ts new file mode 100644 index 0000000000..7c1b6d004a --- /dev/null +++ b/packages/build/src/packaging/download-manpage.ts @@ -0,0 +1,22 @@ +import fetch from 'node-fetch'; +import tar from 'tar'; +import { createReadStream, createWriteStream, promises as fs } from 'fs'; +import { promisify } from 'util'; +import { join } from 'path'; +import { pipeline } from 'stream'; +import { createGzip } from 'zlib'; + +export async function downloadManpage(url: string, destination: string, name: string) { + await fs.mkdir(destination, { recursive: true }); + const response = await fetch(url); + await promisify(pipeline)( + response.body, + tar.x({ cwd: destination }) + ); + await promisify(pipeline)( + createReadStream(join(destination, 'mongosh.1')), + createGzip(), + createWriteStream(join(destination, name)) + ); + console.info(`Saved manpage: ${join(destination, name)}`); +} diff --git a/packages/build/src/packaging/package/debian.spec.ts b/packages/build/src/packaging/package/debian.spec.ts index f19ff57081..a27dfd7e95 100644 --- a/packages/build/src/packaging/package/debian.spec.ts +++ b/packages/build/src/packaging/package/debian.spec.ts @@ -30,6 +30,7 @@ describe('tarball debian', () => { expect(stdout).to.match(/^-rw-r.-r--.+\/usr\/share\/doc\/foobar\/LICENSE_foo$/m); expect(stdout).to.match(/^-rw-r.-r--.+\/usr\/share\/doc\/foobar\/README$/m); expect(stdout).to.match(/^-rw-r.-r--.+\/usr\/share\/doc\/foobar\/copyright$/m); + expect(stdout).to.match(/^-rw-r.-r--.+\/usr\/share\/man\/man1\/mongosh.1.gz$/m); } { const { stdout } = await execFile('dpkg', ['-I', tarball.path]); diff --git a/packages/build/src/packaging/package/debian.ts b/packages/build/src/packaging/package/debian.ts index 5479a9f1e6..84942527b5 100644 --- a/packages/build/src/packaging/package/debian.ts +++ b/packages/build/src/packaging/package/debian.ts @@ -35,6 +35,14 @@ export async function createDebianPackage( for (const { sourceFilePath, packagedFilePath } of docFiles) { await fs.copyFile(sourceFilePath, path.join(docdir, packagedFilePath), COPYFILE_FICLONE); } + + if (pkg.manpage) { + // Put manpage file in /usr/share/man/man1/. + const manualDir = path.join(dir, pkg.metadata.debName, 'usr', 'share', 'man', 'man1'); + await fs.mkdir(manualDir, { recursive: true }); + await fs.copyFile(pkg.manpage.sourceFilePath, path.join(manualDir, pkg.manpage.packagedFilePath), COPYFILE_FICLONE); + } + // Debian packages should contain a 'copyright' file. // https://www.debian.org/doc/debian-policy/ch-archive.html#s-pkgcopyright await fs.writeFile(path.join(docdir, 'copyright'), await generateDebianCopyright(pkg)); diff --git a/packages/build/src/packaging/package/helpers.ts b/packages/build/src/packaging/package/helpers.ts index 11f459733d..ed1bb06070 100644 --- a/packages/build/src/packaging/package/helpers.ts +++ b/packages/build/src/packaging/package/helpers.ts @@ -34,8 +34,12 @@ export async function createCompressedArchiveContents(archiveRootName: string, p await fs.mkdir(archiveRoot, { recursive: true }); const docFiles = [ ...pkg.otherDocFilePaths, - ...pkg.binaries.map(({ license }) => license) + ...pkg.binaries.map(({ license }) => license), ]; + if (pkg.manpage) { + docFiles.push(pkg.manpage); + } + for (const { sourceFilePath, packagedFilePath } of docFiles) { await fs.copyFile(sourceFilePath, path.join(archiveRoot, packagedFilePath), COPYFILE_FICLONE); } diff --git a/packages/build/src/packaging/package/package-information.ts b/packages/build/src/packaging/package/package-information.ts index e998f41bc4..f440723467 100644 --- a/packages/build/src/packaging/package/package-information.ts +++ b/packages/build/src/packaging/package/package-information.ts @@ -9,6 +9,8 @@ interface LicenseInformation extends DocumentationFile { rpmIdentifier: string; } +type ManPage = DocumentationFile; + // This is filled in by the build config file. export interface PackageInformation { binaries: { @@ -17,6 +19,7 @@ export interface PackageInformation { license: LicenseInformation; }[]; otherDocFilePaths: DocumentationFile[]; + manpage?: ManPage; metadata: { name: string; debName: string; diff --git a/packages/build/src/packaging/package/redhat.spec.ts b/packages/build/src/packaging/package/redhat.spec.ts index 2e215e7af2..ac6e1998b8 100644 --- a/packages/build/src/packaging/package/redhat.spec.ts +++ b/packages/build/src/packaging/package/redhat.spec.ts @@ -38,6 +38,7 @@ describe('tarball redhat', () => { expect(stdout).to.match(/^\/usr\/share\/doc\/foobar-1.0.0\/README$/m); expect(stdout).to.match(/^\/usr\/share\/licenses\/foobar-1.0.0\/LICENSE_bar$/m); expect(stdout).to.match(/^\/usr\/share\/licenses\/foobar-1.0.0\/LICENSE_foo$/m); + expect(stdout).to.match(/^\/usr\/share\/man\/man1\/mongosh.1.gz$/m); }); it('determines and copies created RPM', async() => { diff --git a/packages/build/src/packaging/package/redhat.ts b/packages/build/src/packaging/package/redhat.ts index f3d9b87194..d538c51d61 100644 --- a/packages/build/src/packaging/package/redhat.ts +++ b/packages/build/src/packaging/package/redhat.ts @@ -35,14 +35,17 @@ export async function createRedhatPackage( const filelistRpm = [ ...pkg.binaries.map(({ sourceFilePath, category }) => `%{_${category}dir}/${path.basename(sourceFilePath)}`), ...pkg.binaries.map(({ license }) => `%license ${license.packagedFilePath}`), - ...pkg.otherDocFilePaths.map(({ packagedFilePath }) => `%doc ${packagedFilePath}`) - ].join('\n'); + ...pkg.otherDocFilePaths.map(({ packagedFilePath }) => `%doc ${packagedFilePath}`), + ]; + if (pkg.manpage) { + filelistRpm.push(`%doc ${pkg.manpage.packagedFilePath}`); + } const version = sanitizeVersion(pkg.metadata.version, 'rpm'); const dir = await generateDirFromTemplate(templateDir, { ...pkg.metadata, licenseRpm, installscriptRpm, - filelistRpm, + filelistRpm: filelistRpm.join('\n'), version }); // Copy all files that we want to ship into the BUILD directory. @@ -56,6 +59,10 @@ export async function createRedhatPackage( await fs.copyFile(sourceFilePath, path.join(dir, 'BUILD', packagedFilePath), COPYFILE_FICLONE); } + if (pkg.manpage) { + await fs.copyFile(pkg.manpage.sourceFilePath, path.join(dir, 'BUILD', pkg.manpage.packagedFilePath), COPYFILE_FICLONE); + } + // Create the package. await execFile('rpmbuild', [ '-bb', path.join(dir, 'SPECS', `${pkg.metadata.rpmName}.spec`), diff --git a/packages/build/src/packaging/package/zip.spec.ts b/packages/build/src/packaging/package/zip.spec.ts index c1c0ba037f..d3801b3113 100644 --- a/packages/build/src/packaging/package/zip.spec.ts +++ b/packages/build/src/packaging/package/zip.spec.ts @@ -30,7 +30,7 @@ describe('package zip', () => { expect(unzip.stderr).to.be.empty; const lines = unzip.stdout.split('\n'); - expect(lines).to.have.length(13); + expect(lines).to.have.length(14); for (let i = 3; i < 10; i++) { const filename = /([^\s]+)$/.exec(lines[i])?.[1] ?? ''; diff --git a/packages/build/src/packaging/run-package.ts b/packages/build/src/packaging/run-package.ts index e59ea4f11e..12600e6522 100644 --- a/packages/build/src/packaging/run-package.ts +++ b/packages/build/src/packaging/run-package.ts @@ -3,6 +3,7 @@ import path from 'path'; import os from 'os'; import { Config, Platform, validateBuildVariant } from '../config'; import { downloadMongocrypt } from './download-mongocryptd'; +import { downloadManpage } from './download-manpage'; import { macOSSignAndNotarize } from './macos-sign'; import { notarizeMsi } from './msi-sign'; import { createPackage, PackageFile } from './package'; @@ -28,6 +29,15 @@ export async function runPackage( fsConstants.COPYFILE_FICLONE); } + const { manpage } = config; + if (manpage) { + await downloadManpage( + manpage.sourceUrl, + manpage.downloadPath, + manpage.fileName + ); + } + const runCreatePackage = async(): Promise => { return await createPackage( config.outputDir, diff --git a/packages/build/test/fixtures/manpages.tar.gz b/packages/build/test/fixtures/manpages.tar.gz new file mode 100644 index 0000000000..f3d1428221 Binary files /dev/null and b/packages/build/test/fixtures/manpages.tar.gz differ diff --git a/packages/build/test/fixtures/pkgconf.js b/packages/build/test/fixtures/pkgconf.js index e2c6b0d5a8..8fcc51c23d 100644 --- a/packages/build/test/fixtures/pkgconf.js +++ b/packages/build/test/fixtures/pkgconf.js @@ -31,6 +31,10 @@ module.exports = { packagedFilePath: 'README' }, ], + manpage: { + sourceFilePath: path.resolve(__dirname, 'manpages.tar.gz'), + packagedFilePath: 'mongosh.1.gz' + }, metadata: { version: '1.0.0', fullName: 'Very dumb dummy package',