deboa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/cjs/classes/Deboa.js +490 -0
- package/dist/cjs/classes/DeboaFromFile.js +66 -0
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/types/IAddTarEntriesParams.js +4 -0
- package/dist/cjs/types/IControlFileOptions.js +4 -0
- package/dist/cjs/types/IDeboa.js +4 -0
- package/dist/cjs/types/IDeboaFromFile.js +4 -0
- package/dist/cjs/types/INormalizeOptionsLength.js +4 -0
- package/dist/cjs/types/IWriteFileFromLinesArgs.js +4 -0
- package/dist/cjs/types/IWriteToArchive.js +4 -0
- package/dist/cjs/types/MaintainerScript.js +4 -0
- package/dist/cjs/types/Priority.js +4 -0
- package/dist/cjs/types/Section.js +4 -0
- package/dist/cjs/types/classes/Deboa.d.ts +44 -0
- package/dist/cjs/types/classes/DeboaFromFile.d.ts +23 -0
- package/dist/cjs/types/index.d.ts +4 -0
- package/dist/cjs/types/index.js +25 -0
- package/dist/cjs/types/types/IAddTarEntriesParams.d.ts +5 -0
- package/dist/cjs/types/types/IControlFileOptions.d.ts +130 -0
- package/dist/cjs/types/types/IDeboa.d.ts +123 -0
- package/dist/cjs/types/types/IDeboaFromFile.d.ts +8 -0
- package/dist/cjs/types/types/INormalizeOptionsLength.d.ts +8 -0
- package/dist/cjs/types/types/IWriteFileFromLinesArgs.d.ts +10 -0
- package/dist/cjs/types/types/IWriteToArchive.d.ts +19 -0
- package/dist/cjs/types/types/MaintainerScript.d.ts +1 -0
- package/dist/cjs/types/types/Priority.d.ts +1 -0
- package/dist/cjs/types/types/Section.d.ts +1 -0
- package/dist/cjs/types/types/index.d.ts +10 -0
- package/dist/cjs/types/utils/addTarEntries.d.ts +5 -0
- package/dist/cjs/types/utils/createFileHeader.d.ts +2 -0
- package/dist/cjs/types/utils/writeFileFromLines.d.ts +5 -0
- package/dist/cjs/types/utils/writeToArchive.d.ts +5 -0
- package/dist/cjs/utils/addTarEntries.js +25 -0
- package/dist/cjs/utils/createFileHeader.js +61 -0
- package/dist/cjs/utils/writeFileFromLines.js +13 -0
- package/dist/cjs/utils/writeToArchive.js +78 -0
- package/dist/esm/classes/Deboa.js +439 -0
- package/dist/esm/classes/DeboaFromFile.js +55 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/types/IAddTarEntriesParams.js +1 -0
- package/dist/esm/types/IControlFileOptions.js +1 -0
- package/dist/esm/types/IDeboa.js +1 -0
- package/dist/esm/types/IDeboaFromFile.js +1 -0
- package/dist/esm/types/INormalizeOptionsLength.js +1 -0
- package/dist/esm/types/IWriteFileFromLinesArgs.js +4 -0
- package/dist/esm/types/IWriteToArchive.js +1 -0
- package/dist/esm/types/MaintainerScript.js +1 -0
- package/dist/esm/types/Priority.js +1 -0
- package/dist/esm/types/Section.js +1 -0
- package/dist/esm/types/classes/Deboa.d.ts +44 -0
- package/dist/esm/types/classes/DeboaFromFile.d.ts +23 -0
- package/dist/esm/types/index.d.ts +4 -0
- package/dist/esm/types/index.js +10 -0
- package/dist/esm/types/types/IAddTarEntriesParams.d.ts +5 -0
- package/dist/esm/types/types/IControlFileOptions.d.ts +130 -0
- package/dist/esm/types/types/IDeboa.d.ts +123 -0
- package/dist/esm/types/types/IDeboaFromFile.d.ts +8 -0
- package/dist/esm/types/types/INormalizeOptionsLength.d.ts +8 -0
- package/dist/esm/types/types/IWriteFileFromLinesArgs.d.ts +10 -0
- package/dist/esm/types/types/IWriteToArchive.d.ts +19 -0
- package/dist/esm/types/types/MaintainerScript.d.ts +1 -0
- package/dist/esm/types/types/Priority.d.ts +1 -0
- package/dist/esm/types/types/Section.d.ts +1 -0
- package/dist/esm/types/types/index.d.ts +10 -0
- package/dist/esm/types/utils/addTarEntries.d.ts +5 -0
- package/dist/esm/types/utils/createFileHeader.d.ts +2 -0
- package/dist/esm/types/utils/writeFileFromLines.d.ts +5 -0
- package/dist/esm/types/utils/writeToArchive.d.ts +5 -0
- package/dist/esm/utils/addTarEntries.js +19 -0
- package/dist/esm/utils/createFileHeader.js +53 -0
- package/dist/esm/utils/writeFileFromLines.js +7 -0
- package/dist/esm/utils/writeToArchive.js +72 -0
- package/package.json +57 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "createFileHeader", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: ()=>createFileHeader
|
|
8
|
+
});
|
|
9
|
+
function createFileHeader(options) {
|
|
10
|
+
const { fileName , ...numericOptions } = options;
|
|
11
|
+
const { timestamp =String(Math.trunc(new Date().getTime() / 1000)) , ownerID ='0' , groupID ='0' , fileMode ='100644' , fileSize , } = numericOptions;
|
|
12
|
+
if (!fileName) {
|
|
13
|
+
throw new Error('filename is required');
|
|
14
|
+
}
|
|
15
|
+
const maxLengthMap = {
|
|
16
|
+
fileName: {
|
|
17
|
+
maxLength: 16,
|
|
18
|
+
value: fileName
|
|
19
|
+
},
|
|
20
|
+
timestamp: {
|
|
21
|
+
maxLength: 12,
|
|
22
|
+
value: String(timestamp)
|
|
23
|
+
},
|
|
24
|
+
ownerID: {
|
|
25
|
+
maxLength: 6,
|
|
26
|
+
value: String(ownerID)
|
|
27
|
+
},
|
|
28
|
+
groupID: {
|
|
29
|
+
maxLength: 6,
|
|
30
|
+
value: String(groupID)
|
|
31
|
+
},
|
|
32
|
+
fileMode: {
|
|
33
|
+
maxLength: 8,
|
|
34
|
+
value: String(fileMode)
|
|
35
|
+
},
|
|
36
|
+
fileSize: {
|
|
37
|
+
maxLength: 10,
|
|
38
|
+
value: String(fileSize)
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const paddedFields = Object.entries(maxLengthMap).map(([optionName, { maxLength , value }])=>{
|
|
42
|
+
// checks if the numeric options are correct
|
|
43
|
+
if (optionName in numericOptions) {
|
|
44
|
+
const asNumber = +value;
|
|
45
|
+
if (Number.isNaN(asNumber)) {
|
|
46
|
+
throw new Error(`The \`${optionName}\` option must be a numeric value`);
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isInteger(asNumber)) {
|
|
49
|
+
throw new Error(`The \`${optionName}\` option must be a integer`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// checks if the option length is correct
|
|
53
|
+
if (value.length > maxLength) {
|
|
54
|
+
throw new Error(`The \`${optionName}\` option must be at most ${maxLength} characters`);
|
|
55
|
+
}
|
|
56
|
+
return value.padEnd(maxLength);
|
|
57
|
+
});
|
|
58
|
+
// ending sequence
|
|
59
|
+
paddedFields.push('`\n');
|
|
60
|
+
return paddedFields.join('');
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "writeFileFromLines", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: ()=>writeFileFromLines
|
|
8
|
+
});
|
|
9
|
+
const _fs = require("fs");
|
|
10
|
+
async function writeFileFromLines({ filePath , lines }) {
|
|
11
|
+
const joined = lines.join('\n') + '\n';
|
|
12
|
+
await _fs.promises.writeFile(filePath, joined);
|
|
13
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "writeToArchive", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: ()=>writeToArchive
|
|
8
|
+
});
|
|
9
|
+
const _createFileHeader = require("./createFileHeader");
|
|
10
|
+
/**
|
|
11
|
+
* Writes the .ar archive signature. If it's a .deb file,
|
|
12
|
+
* also writes the `debian-control` file and its header.
|
|
13
|
+
*/ async function writeArchiveHeader({ writeStream , isARFile =false }) {
|
|
14
|
+
return new Promise((resolve, reject)=>{
|
|
15
|
+
const arArchiveSignature = '!<arch>\n';
|
|
16
|
+
const header = [
|
|
17
|
+
arArchiveSignature
|
|
18
|
+
];
|
|
19
|
+
// this part is .deb-specific, so we don't need it in plain .ar files
|
|
20
|
+
if (!isARFile) {
|
|
21
|
+
const debianBinaryFile = '2.0\n';
|
|
22
|
+
const debianBinaryIdentifier = (0, _createFileHeader.createFileHeader)({
|
|
23
|
+
fileName: 'debian-binary',
|
|
24
|
+
fileSize: debianBinaryFile.length
|
|
25
|
+
});
|
|
26
|
+
header.push(debianBinaryIdentifier, debianBinaryFile);
|
|
27
|
+
}
|
|
28
|
+
writeStream.write(Buffer.concat(header.map(Buffer.from)), (headerErr)=>{
|
|
29
|
+
if (headerErr) {
|
|
30
|
+
reject(`Error writing the archive header: ${headerErr}`);
|
|
31
|
+
}
|
|
32
|
+
resolve();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function writeToArchive({ header , isARFile =false , readStream , writeStream }) {
|
|
37
|
+
const shouldWriteArchiveHeader = writeStream.bytesWritten === 0;
|
|
38
|
+
if (shouldWriteArchiveHeader) {
|
|
39
|
+
console.log('Writing the archive header...\n');
|
|
40
|
+
await writeArchiveHeader({
|
|
41
|
+
writeStream,
|
|
42
|
+
isARFile
|
|
43
|
+
});
|
|
44
|
+
console.log('Archive header successfully written\n');
|
|
45
|
+
}
|
|
46
|
+
return new Promise((resolve, reject)=>{
|
|
47
|
+
const fileNameIdentifier = header.toString('utf-8').slice(0, 16).trim();
|
|
48
|
+
writeStream.write(header, (identifierErr)=>{
|
|
49
|
+
console.log(`Writing the identifier header for ${fileNameIdentifier}...\n`);
|
|
50
|
+
if (identifierErr) {
|
|
51
|
+
reject(`Error writing the identifier header for ${fileNameIdentifier}: ${identifierErr}`);
|
|
52
|
+
}
|
|
53
|
+
console.log(`Identifier header for ${fileNameIdentifier} successfully written\n`);
|
|
54
|
+
const initialBytesWritten = writeStream.bytesWritten;
|
|
55
|
+
console.log(`Writing file ${fileNameIdentifier}...\n`);
|
|
56
|
+
readStream.on('error', (fileError)=>reject(`Error writing file ${fileNameIdentifier}: ${fileError}`));
|
|
57
|
+
readStream.pipe(writeStream, {
|
|
58
|
+
end: false
|
|
59
|
+
});
|
|
60
|
+
readStream.on('end', ()=>{
|
|
61
|
+
const readStreamSize = writeStream.bytesWritten - initialBytesWritten;
|
|
62
|
+
const shouldWritePaddingByte = readStreamSize % 2 !== 0;
|
|
63
|
+
if (!shouldWritePaddingByte) {
|
|
64
|
+
console.log(`File ${fileNameIdentifier} successfully written\n`);
|
|
65
|
+
return resolve();
|
|
66
|
+
}
|
|
67
|
+
console.log(`Writing padding byte for ${fileNameIdentifier}...\n`);
|
|
68
|
+
writeStream.write(Buffer.from('\n'), (paddingError)=>{
|
|
69
|
+
if (paddingError) {
|
|
70
|
+
reject(`Error writing padding byte for ${fileNameIdentifier}: ${identifierErr}`);
|
|
71
|
+
}
|
|
72
|
+
console.log(`File ${fileNameIdentifier} successfully written\n`);
|
|
73
|
+
return resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { once } from 'events';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import fsExtra from 'fs-extra';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import fastFolderSize from 'fast-folder-size';
|
|
8
|
+
import tar from 'tar-fs';
|
|
9
|
+
import { writeFileFromLines } from '../utils/writeFileFromLines';
|
|
10
|
+
import { DeboaFromFile } from './DeboaFromFile';
|
|
11
|
+
import { PassThrough as PassThroughStream } from 'stream';
|
|
12
|
+
import { addTarEntries } from '../utils/addTarEntries';
|
|
13
|
+
/**
|
|
14
|
+
* @return IDeboa
|
|
15
|
+
*/ class Deboa {
|
|
16
|
+
/** See {@link IDeboa.additionalTarEntries} */ additionalTarEntries = [];
|
|
17
|
+
/** See {@link IDeboa.beforeCreateDesktopEntry} */ beforeCreateDesktopEntry = null;
|
|
18
|
+
/** See {@link IDeboa.beforePackage} */ beforePackage = null;
|
|
19
|
+
/** See {@link IDeboa.controlFileOptions} */ controlFileOptions = {
|
|
20
|
+
/** See {@link IDeboa.architecture} */ architecture: null,
|
|
21
|
+
/** See {@link IDeboa.builtUsing} */ builtUsing: null,
|
|
22
|
+
/** See {@link IDeboa.conflicts} */ conflicts: null,
|
|
23
|
+
/** See {@link IDeboa.depends} */ depends: null,
|
|
24
|
+
/** See {@link IDeboa.essential} */ essential: null,
|
|
25
|
+
/** See {@link IDeboa.extendedDescription} */ extendedDescription: null,
|
|
26
|
+
/** See {@link IDeboa.homepage} */ homepage: null,
|
|
27
|
+
/** See {@link IDeboa.maintainer} */ maintainer: null,
|
|
28
|
+
/** See {@link IDeboa.maintainerScripts} */ maintainerScripts: {
|
|
29
|
+
preinst: null,
|
|
30
|
+
postinst: null,
|
|
31
|
+
prerm: null,
|
|
32
|
+
postrm: null
|
|
33
|
+
},
|
|
34
|
+
/** See {@link IDeboa.packageName} */ packageName: null,
|
|
35
|
+
/** See {@link IDeboa.preDepends} */ preDepends: null,
|
|
36
|
+
/** See {@link IDeboa.priority} */ priority: null,
|
|
37
|
+
/** See {@link IDeboa.recommends} */ recommends: null,
|
|
38
|
+
/** See {@link IDeboa.section} */ section: null,
|
|
39
|
+
/** See {@link IDeboa.shortDescription} */ shortDescription: null,
|
|
40
|
+
/** See {@link IDeboa.source} */ source: null,
|
|
41
|
+
/** See {@link IDeboa.suggests} */ suggests: null,
|
|
42
|
+
/** See {@link IDeboa.version} */ version: null
|
|
43
|
+
};
|
|
44
|
+
/** See {@link IDeboa.icon} */ icon = null;
|
|
45
|
+
/** See {@link IDeboa.modifyTarHeader} */ modifyTarHeader = null;
|
|
46
|
+
/** See {@link IDeboa.sourceDir} */ sourceDir = null;
|
|
47
|
+
/** See {@link IDeboa.tarballFormat} */ tarballFormat = null;
|
|
48
|
+
/** See {@link IDeboa.targetDir} */ targetDir = null;
|
|
49
|
+
#appFolderDestination = null;
|
|
50
|
+
#controlFolderDestination = null;
|
|
51
|
+
#dataFolderDestination = null;
|
|
52
|
+
#hooksLoaded = false;
|
|
53
|
+
#outputFile = null;
|
|
54
|
+
#tempDir = null;
|
|
55
|
+
constructor(options){
|
|
56
|
+
if (!Object.keys(options || {}).length) {
|
|
57
|
+
throw new Error('No configuration options provided');
|
|
58
|
+
}
|
|
59
|
+
const { sourceDir , targetDir } = options;
|
|
60
|
+
if (!sourceDir) {
|
|
61
|
+
throw new Error('The `sourceDir` field is mandatory');
|
|
62
|
+
}
|
|
63
|
+
if (!targetDir) {
|
|
64
|
+
throw new Error('The `targetDir` field is mandatory');
|
|
65
|
+
}
|
|
66
|
+
Deboa.#validateOptions(options);
|
|
67
|
+
const { controlFileOptions } = options;
|
|
68
|
+
const osArch = os.arch();
|
|
69
|
+
// default values
|
|
70
|
+
options = {
|
|
71
|
+
...options,
|
|
72
|
+
controlFileOptions: {
|
|
73
|
+
...controlFileOptions,
|
|
74
|
+
...!controlFileOptions.architecture && {
|
|
75
|
+
architecture: osArch === 'x64' ? 'amd64' : osArch
|
|
76
|
+
},
|
|
77
|
+
...!controlFileOptions.maintainerScripts && {
|
|
78
|
+
maintainerScripts: {}
|
|
79
|
+
},
|
|
80
|
+
...!controlFileOptions.priority && {
|
|
81
|
+
priority: 'optional'
|
|
82
|
+
},
|
|
83
|
+
...!controlFileOptions.extendedDescription && {
|
|
84
|
+
extendedDescription: controlFileOptions.shortDescription
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
tarballFormat: [
|
|
88
|
+
'tar',
|
|
89
|
+
'tar.gz',
|
|
90
|
+
'tar.xz'
|
|
91
|
+
].includes(options.tarballFormat) ? options.tarballFormat : 'tar.gz'
|
|
92
|
+
};
|
|
93
|
+
for (const [property, value] of Object.entries(options)){
|
|
94
|
+
this[property] = value;
|
|
95
|
+
}
|
|
96
|
+
this.#tempDir = path.join(os.tmpdir(), 'deboa_temp');
|
|
97
|
+
const { architecture , packageName , version } = this.controlFileOptions;
|
|
98
|
+
this.#outputFile = path.join(this.targetDir, `${packageName}_${version}_${architecture}.deb`);
|
|
99
|
+
this.#controlFolderDestination = path.join(this.#tempDir, 'control');
|
|
100
|
+
this.#dataFolderDestination = path.join(this.#tempDir, 'data');
|
|
101
|
+
this.#appFolderDestination = path.join(this.#dataFolderDestination, 'usr', 'lib', packageName);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Ensures that all required options are present and that the `packageName`
|
|
105
|
+
* and `maintainer` fields are provided in the right format.
|
|
106
|
+
*/ static #validateOptions(options) {
|
|
107
|
+
console.log('Validating options...\n');
|
|
108
|
+
const { controlFileOptions: { shortDescription , maintainer , packageName , version , } , } = options;
|
|
109
|
+
if (!packageName) {
|
|
110
|
+
throw new Error('The controlFileOptions.`packageName` field is mandatory');
|
|
111
|
+
}
|
|
112
|
+
if (!version) {
|
|
113
|
+
throw new Error('The `controlFileOptions.version` field is mandatory');
|
|
114
|
+
}
|
|
115
|
+
if (!maintainer) {
|
|
116
|
+
throw new Error('The `controlFileOptions.maintainer` field is mandatory');
|
|
117
|
+
}
|
|
118
|
+
if (!shortDescription) {
|
|
119
|
+
throw new Error('The `controlFileOptions.description` field is mandatory');
|
|
120
|
+
}
|
|
121
|
+
if (packageName.length < 2) {
|
|
122
|
+
throw new Error('The `controlFileOptions.packageName` field must be at least two characters long');
|
|
123
|
+
}
|
|
124
|
+
if (packageName.replace(/[^a-z\d\-+.]/g, '').replace(/^[-+.]/g, '').toLowerCase() !== packageName) {
|
|
125
|
+
throw new Error('The `controlFileOptions.packageName` field contains illegal characters');
|
|
126
|
+
}
|
|
127
|
+
/*
|
|
128
|
+
if (!maintainer.match(/^(\p{L}+ ?)+ ?<(.*?)@(.*?)>$/u)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
'The `controlFileOptions.maintainer` field does not match the expected format `John Doe <johndoe@example.com>`',
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
*/ const { postinst , prerm , postrm , preinst } = options.controlFileOptions.maintainerScripts || {};
|
|
134
|
+
for (const [scriptName, scriptPath] of Object.entries({
|
|
135
|
+
postinst,
|
|
136
|
+
postrm,
|
|
137
|
+
preinst,
|
|
138
|
+
prerm
|
|
139
|
+
})){
|
|
140
|
+
if (scriptPath) {
|
|
141
|
+
try {
|
|
142
|
+
fs.accessSync(path.resolve(scriptPath));
|
|
143
|
+
} catch (e) {
|
|
144
|
+
throw new Error(`Error accessing \`${scriptPath}\` (provided in \`controlFileOptions.maintainerScripts.${scriptName}\`). Make sure that this file exists and is accessible by the current user.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Creates the .deb file.
|
|
151
|
+
* @return {Promise<string>} outputFile - Absolute path to the generated .deb
|
|
152
|
+
*/ async package() {
|
|
153
|
+
const startTime = process.hrtime.bigint();
|
|
154
|
+
const tempDir = this.#tempDir;
|
|
155
|
+
await fsExtra.ensureDir(tempDir);
|
|
156
|
+
if (!this.#hooksLoaded) {
|
|
157
|
+
await this.loadHooks();
|
|
158
|
+
}
|
|
159
|
+
await this.#copyFolderTree();
|
|
160
|
+
await this.#copyMaintainerScripts();
|
|
161
|
+
await this.#copyPackageFiles();
|
|
162
|
+
await this.#createControlFile();
|
|
163
|
+
await this.#copyIconAndDesktopEntryFile();
|
|
164
|
+
if (typeof this.beforePackage === 'function') {
|
|
165
|
+
await this.beforePackage(this.#dataFolderDestination);
|
|
166
|
+
}
|
|
167
|
+
await this.#createTarballs();
|
|
168
|
+
await this.#createDeb();
|
|
169
|
+
console.log('Removing temporary files...\n');
|
|
170
|
+
await fs.promises.rm(this.#tempDir, {
|
|
171
|
+
recursive: true
|
|
172
|
+
});
|
|
173
|
+
const endTime = process.hrtime.bigint();
|
|
174
|
+
const duration = parseInt(String(endTime - startTime));
|
|
175
|
+
console.log(`.deb created in ${(duration / 1e9).toFixed(2)}s`);
|
|
176
|
+
return this.#outputFile;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Creates the tarballs for the control and data files.
|
|
180
|
+
*/ async #createTarballs() {
|
|
181
|
+
console.log('Packaging files....\n');
|
|
182
|
+
const { tarballFormat } = this;
|
|
183
|
+
const dataFileLocation = this.#dataFolderDestination + `.${tarballFormat}`;
|
|
184
|
+
const controlFileLocation = this.#controlFolderDestination + `.${tarballFormat}`;
|
|
185
|
+
let dataFileCompressor;
|
|
186
|
+
let controlFileCompressor;
|
|
187
|
+
switch(tarballFormat){
|
|
188
|
+
case 'tar':
|
|
189
|
+
{
|
|
190
|
+
dataFileCompressor = new PassThroughStream();
|
|
191
|
+
controlFileCompressor = new PassThroughStream();
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case 'tar.gz':
|
|
195
|
+
{
|
|
196
|
+
const zlib = (await import('zlib')).default;
|
|
197
|
+
dataFileCompressor = zlib.createGzip();
|
|
198
|
+
controlFileCompressor = zlib.createGzip();
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case 'tar.xz':
|
|
202
|
+
{
|
|
203
|
+
const lzma = (await import('lzma-native')).default;
|
|
204
|
+
dataFileCompressor = lzma.createCompressor({
|
|
205
|
+
threads: 0
|
|
206
|
+
});
|
|
207
|
+
controlFileCompressor = lzma.createCompressor({
|
|
208
|
+
threads: 0
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const dataFileWriteStream = fs.createWriteStream(dataFileLocation);
|
|
214
|
+
const controlFileWriteStream = fs.createWriteStream(controlFileLocation);
|
|
215
|
+
tar.pack(this.#dataFolderDestination, {
|
|
216
|
+
map: (header)=>{
|
|
217
|
+
// sensible defaults for Windows users
|
|
218
|
+
if (process.platform === 'win32') {
|
|
219
|
+
const defaultFilePermission = parseInt('0644', 8);
|
|
220
|
+
const defaultFolderPermission = parseInt('0755', 8);
|
|
221
|
+
switch(header.type){
|
|
222
|
+
case 'file':
|
|
223
|
+
{
|
|
224
|
+
header.mode = defaultFilePermission;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'directory':
|
|
228
|
+
{
|
|
229
|
+
header.mode = defaultFolderPermission;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (typeof this.modifyTarHeader === 'function') {
|
|
235
|
+
header = this.modifyTarHeader(header);
|
|
236
|
+
}
|
|
237
|
+
return header;
|
|
238
|
+
},
|
|
239
|
+
...this.additionalTarEntries.length && {
|
|
240
|
+
finalize: false,
|
|
241
|
+
finish: async (pack)=>{
|
|
242
|
+
await addTarEntries({
|
|
243
|
+
entries: this.additionalTarEntries,
|
|
244
|
+
pack
|
|
245
|
+
});
|
|
246
|
+
pack.finalize();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}).pipe(dataFileCompressor).pipe(dataFileWriteStream);
|
|
250
|
+
tar.pack(this.#controlFolderDestination, {
|
|
251
|
+
map: (header)=>{
|
|
252
|
+
const maintainerScripts = [
|
|
253
|
+
'postinst',
|
|
254
|
+
'postrm',
|
|
255
|
+
'preinst',
|
|
256
|
+
'prerm',
|
|
257
|
+
];
|
|
258
|
+
// maintainer scripts must be executable
|
|
259
|
+
if (maintainerScripts.includes(header.name)) {
|
|
260
|
+
header.mode = parseInt('0755', 8);
|
|
261
|
+
}
|
|
262
|
+
return header;
|
|
263
|
+
}
|
|
264
|
+
}).pipe(controlFileCompressor).pipe(controlFileWriteStream);
|
|
265
|
+
await Promise.all([
|
|
266
|
+
once(dataFileWriteStream, 'finish'),
|
|
267
|
+
once(controlFileWriteStream, 'finish'),
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Copies the empty folders to the temporary location.
|
|
272
|
+
*/ async #copyFolderTree() {
|
|
273
|
+
console.log('Creating directory structure in the temporary folder...\n');
|
|
274
|
+
await fsExtra.ensureDir(this.#appFolderDestination);
|
|
275
|
+
await fsExtra.ensureDir(this.#controlFolderDestination);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Copies the source files to the temporary folder.
|
|
279
|
+
*/ async #copyPackageFiles() {
|
|
280
|
+
console.log('Copying source directory...\n');
|
|
281
|
+
await fsExtra.copy(this.sourceDir, this.#appFolderDestination);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Copies the maintainer scripts to the temporary folder.
|
|
285
|
+
*/ async #copyMaintainerScripts() {
|
|
286
|
+
console.log('Copying maintainer scripts...\n');
|
|
287
|
+
const { controlFileOptions: { maintainerScripts: { postinst: postinst1 , postrm: postrm1 , preinst: preinst1 , prerm: prerm1 } = {} , } , } = this;
|
|
288
|
+
const scripts = Object.entries({
|
|
289
|
+
postinst: postinst1,
|
|
290
|
+
postrm: postrm1,
|
|
291
|
+
preinst: preinst1,
|
|
292
|
+
prerm: prerm1
|
|
293
|
+
}).filter(([, scriptPath])=>scriptPath);
|
|
294
|
+
for (const [scriptName1, scriptPath1] of scripts){
|
|
295
|
+
await fs.promises.copyFile(path.resolve(scriptPath1), path.join(this.#controlFolderDestination, scriptName1));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Creates the control file and writes it to the temporary folder.
|
|
300
|
+
*/ async #createControlFile() {
|
|
301
|
+
console.log('Creating control file...\n');
|
|
302
|
+
const { controlFileOptions: { packageName: packageName1 , version: version1 , section , priority , architecture , maintainer: maintainer1 , homepage , suggests =[] , depends =[] , recommends =[] , shortDescription: shortDescription1 , extendedDescription , builtUsing , conflicts =[] , essential , preDepends =[] , } , } = this;
|
|
303
|
+
const fastFolderSizeAsync = promisify(fastFolderSize);
|
|
304
|
+
const installedSize = await fastFolderSizeAsync(this.sourceDir);
|
|
305
|
+
const lines = [
|
|
306
|
+
`Package: ${packageName1}`,
|
|
307
|
+
`Version: ${version1}`,
|
|
308
|
+
section && `Section: ${section}`,
|
|
309
|
+
`Priority: ${priority}`,
|
|
310
|
+
`Architecture: ${architecture}`,
|
|
311
|
+
`Maintainer: ${maintainer1}`,
|
|
312
|
+
Array.isArray(depends) && depends.length && `Depends: ${depends.join(', ')}`,
|
|
313
|
+
preDepends.length && `Pre-Depends: ${preDepends.join(', ')}`,
|
|
314
|
+
recommends.length && `Recommends: ${recommends.join(', ')}`,
|
|
315
|
+
suggests.length && `Suggests: ${suggests.join(', ')}`,
|
|
316
|
+
conflicts.length && `Conflicts: ${conflicts.join(', ')}`,
|
|
317
|
+
`Installed-Size: ${Math.ceil(installedSize / 1024)}`,
|
|
318
|
+
homepage && `Homepage: ${homepage}`,
|
|
319
|
+
builtUsing && `Built-Using: ${builtUsing}`,
|
|
320
|
+
essential && `Essential: ${essential}`,
|
|
321
|
+
`Description: ${shortDescription1}`,
|
|
322
|
+
` ${extendedDescription}`,
|
|
323
|
+
].filter(Boolean);
|
|
324
|
+
await writeFileFromLines({
|
|
325
|
+
filePath: path.join(this.#controlFolderDestination, 'control'),
|
|
326
|
+
lines
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Copies the provided icon to the temporary folder
|
|
331
|
+
* and writes the desktop entry file.
|
|
332
|
+
*/ async #copyIconAndDesktopEntryFile() {
|
|
333
|
+
let iconFileExists = false;
|
|
334
|
+
let iconDestination = null;
|
|
335
|
+
const { packageName: packageName2 , shortDescription: shortDescription2 } = this.controlFileOptions;
|
|
336
|
+
if (this.icon) {
|
|
337
|
+
const iconPath = path.resolve(this.icon);
|
|
338
|
+
iconFileExists = await fsExtra.pathExists(iconPath);
|
|
339
|
+
const { ext: extension } = path.parse(iconPath);
|
|
340
|
+
if (iconFileExists) {
|
|
341
|
+
iconDestination = path.join(this.#dataFolderDestination, 'usr/share/pixmaps', packageName2 + extension);
|
|
342
|
+
await fsExtra.ensureDir(path.resolve(iconDestination, '../'));
|
|
343
|
+
await fsExtra.copy(iconPath, iconDestination);
|
|
344
|
+
console.log(`App icon saved to ${iconDestination}`);
|
|
345
|
+
} else {
|
|
346
|
+
console.warn(`\nWARNING: file \`${iconPath}\` not found, skipping app icon...\n`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let desktopEntries = {
|
|
350
|
+
Comment: shortDescription2,
|
|
351
|
+
GenericName: packageName2,
|
|
352
|
+
Name: packageName2,
|
|
353
|
+
Type: 'Application',
|
|
354
|
+
...iconFileExists && {
|
|
355
|
+
Icon: packageName2
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
if (typeof this.beforeCreateDesktopEntry === 'function') {
|
|
359
|
+
desktopEntries = await this.beforeCreateDesktopEntry(desktopEntries);
|
|
360
|
+
}
|
|
361
|
+
if (!Object.keys(desktopEntries).length) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const lines1 = Object.entries(desktopEntries).reduce((acc, entry)=>[
|
|
365
|
+
...acc,
|
|
366
|
+
entry.join('=')
|
|
367
|
+
], []);
|
|
368
|
+
const desktopFileDestination = path.join(this.#dataFolderDestination, 'usr/share/applications', `${packageName2}.desktop`);
|
|
369
|
+
await fsExtra.ensureDir(path.resolve(desktopFileDestination, '../'));
|
|
370
|
+
await writeFileFromLines({
|
|
371
|
+
filePath: desktopFileDestination,
|
|
372
|
+
lines: [
|
|
373
|
+
'[Desktop Entry]',
|
|
374
|
+
...lines1
|
|
375
|
+
]
|
|
376
|
+
});
|
|
377
|
+
console.log(`Desktop entries file saved to ${desktopFileDestination}`);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Checks if the values provided to the hook options are file paths
|
|
381
|
+
* and imports the actual functions from them if necessary.
|
|
382
|
+
*/ async loadHooks() {
|
|
383
|
+
const { beforePackage , modifyTarHeader , beforeCreateDesktopEntry } = this;
|
|
384
|
+
const hooks = {
|
|
385
|
+
beforeCreateDesktopEntry,
|
|
386
|
+
beforePackage,
|
|
387
|
+
modifyTarHeader
|
|
388
|
+
};
|
|
389
|
+
for (const [hookName, hookValue] of Object.entries(hooks)){
|
|
390
|
+
if (hookValue) {
|
|
391
|
+
switch(typeof hookValue){
|
|
392
|
+
case 'function':
|
|
393
|
+
{
|
|
394
|
+
this[hookName] = hookValue;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case 'string':
|
|
398
|
+
{
|
|
399
|
+
const filePath = path.resolve(hookValue);
|
|
400
|
+
const fileExists = await fsExtra.pathExists(filePath);
|
|
401
|
+
if (!fileExists) {
|
|
402
|
+
throw new Error(`The file \`${hookValue}\` doesn't exist or cannot be accessed by the current user.`);
|
|
403
|
+
}
|
|
404
|
+
const importedFn = (await import(filePath)).default;
|
|
405
|
+
if (typeof importedFn === 'function') {
|
|
406
|
+
this[hookName] = importedFn;
|
|
407
|
+
} else {
|
|
408
|
+
throw new Error(`The file \`${filePath}\` must have a function as its default export.`);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
default:
|
|
413
|
+
{
|
|
414
|
+
throw new Error(`Invalid type provided for the \`${hookName}\` option, expected a function or a path to a file that has a function as its default export.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
this.#hooksLoaded = true;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Writes the .deb to the output folder.
|
|
423
|
+
*/ async #createDeb() {
|
|
424
|
+
console.log('Writing .deb file...\n');
|
|
425
|
+
await fsExtra.ensureDir(this.targetDir);
|
|
426
|
+
const deBoaFromFile = new DeboaFromFile({
|
|
427
|
+
outputFile: this.#outputFile
|
|
428
|
+
});
|
|
429
|
+
try {
|
|
430
|
+
await deBoaFromFile.writeFromFile(this.#controlFolderDestination + `.${this.tarballFormat}`);
|
|
431
|
+
await deBoaFromFile.writeFromFile(this.#dataFolderDestination + `.${this.tarballFormat}`);
|
|
432
|
+
deBoaFromFile.writeStream.close();
|
|
433
|
+
} catch (e1) {
|
|
434
|
+
console.log('ERROR: ', e1);
|
|
435
|
+
throw e1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
export { Deboa };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createFileHeader } from '../utils/createFileHeader';
|
|
4
|
+
import { writeToArchive } from '../utils/writeToArchive';
|
|
5
|
+
/**
|
|
6
|
+
* @return IDeboaFromFile
|
|
7
|
+
*/ export class DeboaFromFile {
|
|
8
|
+
#header = null;
|
|
9
|
+
isARFile = false;
|
|
10
|
+
outputFile = null;
|
|
11
|
+
writeStream = null;
|
|
12
|
+
constructor(options){
|
|
13
|
+
const { outputFile , isARFile } = options;
|
|
14
|
+
this.isARFile = isARFile;
|
|
15
|
+
this.outputFile = outputFile;
|
|
16
|
+
this.writeStream = fs.createWriteStream(outputFile, {
|
|
17
|
+
encoding: 'binary'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a ReadStream from the input file.
|
|
22
|
+
* Useful if you need access to the underlying stream.
|
|
23
|
+
*/ async createReadStream(inputFile) {
|
|
24
|
+
if (this.#header !== null) {
|
|
25
|
+
throw new Error('You can only have one ReadStream at a time.');
|
|
26
|
+
}
|
|
27
|
+
const readStream = fs.createReadStream(inputFile, {
|
|
28
|
+
encoding: 'binary'
|
|
29
|
+
});
|
|
30
|
+
const stats = await fs.promises.lstat(inputFile);
|
|
31
|
+
this.#header = createFileHeader({
|
|
32
|
+
fileName: path.basename(inputFile),
|
|
33
|
+
fileSize: stats.size
|
|
34
|
+
});
|
|
35
|
+
return readStream;
|
|
36
|
+
}
|
|
37
|
+
async writeFromStream(readStream) {
|
|
38
|
+
if (this.#header === null) {
|
|
39
|
+
throw new Error('Missing header, please create the ReadStream using the `createReadStream` method.');
|
|
40
|
+
}
|
|
41
|
+
await writeToArchive({
|
|
42
|
+
header: Buffer.from(this.#header),
|
|
43
|
+
isARFile: this.isARFile,
|
|
44
|
+
readStream,
|
|
45
|
+
writeStream: this.writeStream
|
|
46
|
+
});
|
|
47
|
+
this.#header = null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Writes the input file to the .deb.
|
|
51
|
+
*/ async writeFromFile(inputFile) {
|
|
52
|
+
const readStream = await this.createReadStream(inputFile);
|
|
53
|
+
return this.writeFromStream(readStream);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|