The source offer is a link to this fork, not a tarball beside the app (#7)
INBUXA Admin built its own source into every deployment: the whole tree packed as source.tar.gz next to the app, with an identity string shown beside the link and in the version tooltip. The top bar menu and the source link offered that download. It answered the AGPL precisely -- the source of *this* build, uncommitted work and all -- but it paid for that by carrying the tree into every deployment, to a repository that is public and already has it. The version shown beside the link already names the build, so the link and the version together say what the archive said. This is the same change already made to ihasmail-inbuxa, for the same reason, and it leaves the two forks answering the offer the same way. It is also what lets INBUXA Admin be built in an Alpine image: the plugin shelled out to `tar --transform`, which BusyBox tar does not have, so `npm run build` failed there and nowhere else. Removed with it: source-archive.ts, the Vite plugin call, __SOURCE_ID__ and sourceDownloadUrl(). The build no longer shells out to git or tar, and nothing is written next to the app. The two strings that named the archive change with it: the link now reads "Source code (<version>), AGPL-3.0" rather than naming a tree id, and the version tooltip drops the id it appended.
This commit is contained in:
@@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The AGPL's offer, for this build: the exact source it was built from,
|
|
||||||
* written next to the app as `source.tar.gz`, and an identity for it that the
|
|
||||||
* interface shows with the download link.
|
|
||||||
*
|
|
||||||
* "Exact" includes uncommitted work, new files too: every file git doesn't
|
|
||||||
* ignore is written into a throwaway index, never the real one, and the tree
|
|
||||||
* that makes is what gets archived. A clean tree is HEAD's tree. Outside a git
|
|
||||||
* checkout (a release tarball, say), the project files are packed as they are.
|
|
||||||
*/
|
|
||||||
import { execFileSync } from 'node:child_process';
|
|
||||||
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
import path from 'node:path';
|
|
||||||
import type { Plugin } from 'vite';
|
|
||||||
|
|
||||||
const EXCLUDE = ['node_modules', 'dist', '.git', '.ignore', 'coverage'];
|
|
||||||
|
|
||||||
function git(args: string[], cwd: string, env?: NodeJS.ProcessEnv): string {
|
|
||||||
return execFileSync('git', args, { cwd, env: env ?? process.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The tree this build is made from: its git tree id, and a name that says whether it has uncommitted work. */
|
|
||||||
export function sourceIdentity(root: string): { ref: string | null; id: string } {
|
|
||||||
try {
|
|
||||||
git(['rev-parse', '--is-inside-work-tree'], root);
|
|
||||||
} catch {
|
|
||||||
return { ref: null, id: 'unversioned' };
|
|
||||||
}
|
|
||||||
const dir = mkdtempSync(path.join(tmpdir(), 'inbuxa-source-'));
|
|
||||||
try {
|
|
||||||
const env = { ...process.env, GIT_INDEX_FILE: path.join(dir, 'index') };
|
|
||||||
git(['read-tree', 'HEAD'], root, env);
|
|
||||||
git(['add', '--all', '.'], root, env);
|
|
||||||
const tree = git(['write-tree'], root, env);
|
|
||||||
const headTree = git(['rev-parse', 'HEAD^{tree}'], root);
|
|
||||||
const head = git(['rev-parse', '--short=12', 'HEAD'], root);
|
|
||||||
return tree === headTree ? { ref: tree, id: head } : { ref: tree, id: `${head}+local-${tree.slice(0, 12)}` };
|
|
||||||
} finally {
|
|
||||||
rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sourceArchive(root: string, name: string, identity: { ref: string | null; id: string }): Plugin {
|
|
||||||
return {
|
|
||||||
name: 'inbuxa-source-archive',
|
|
||||||
apply: 'build',
|
|
||||||
closeBundle() {
|
|
||||||
const outDir = path.join(root, 'dist');
|
|
||||||
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
||||||
const out = path.join(outDir, 'source.tar.gz');
|
|
||||||
const prefix = `${name}-${identity.id}/`;
|
|
||||||
if (identity.ref) {
|
|
||||||
execFileSync('git', ['archive', '--format=tar.gz', `--prefix=${prefix}`, '-o', out, identity.ref], { cwd: root });
|
|
||||||
} else {
|
|
||||||
execFileSync(
|
|
||||||
'tar',
|
|
||||||
[...EXCLUDE.map((e) => `--exclude=./${e}`), `--transform=s,^\\.,${prefix.slice(0, -1)},`, '-czf', out, '.'],
|
|
||||||
{ cwd: root },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,17 +5,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { sourceDownloadUrl } from '@/lib/sourceDownload';
|
import { SOURCE_URL } from '@/lib/sourceDownload';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AGPL's offer to everyone using this interface over the network: the
|
* The AGPL's offer to everyone using this interface over the network: where
|
||||||
* exact source of the version running, with that version named.
|
* the source is, with the running version named beside it.
|
||||||
*/
|
*/
|
||||||
export function SourceLink({ className }: { className?: string }) {
|
export function SourceLink({ className }: { className?: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<a href={sourceDownloadUrl()} target="_blank" rel="noopener noreferrer" className={className}>
|
<a href={SOURCE_URL} target="_blank" rel="noopener noreferrer" className={className}>
|
||||||
{t('source.download', 'Source code of this version ({{id}}), AGPL-3.0', { id: __SOURCE_ID__ })}
|
{t('source.download', 'Source code ({{version}}), AGPL-3.0', { version: __APP_VERSION__ })}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { isPaletteId, PALETTES } from '@/lib/palettes';
|
|||||||
import Logo from '@/components/common/Logo';
|
import Logo from '@/components/common/Logo';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell';
|
import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell';
|
||||||
import { sourceDownloadUrl } from '@/lib/sourceDownload';
|
import { SOURCE_URL } from '@/lib/sourceDownload';
|
||||||
import { visibleLayouts } from '@/lib/layout';
|
import { visibleLayouts } from '@/lib/layout';
|
||||||
import { sectionLandingLink } from '@/lib/lastVisited';
|
import { sectionLandingLink } from '@/lib/lastVisited';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -104,7 +104,7 @@ export function TopBar() {
|
|||||||
</Link>
|
</Link>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
{t('version.label', 'INBUXA Admin {{version}}', { version: __APP_VERSION__ })} · {__SOURCE_ID__}
|
{t('version.label', 'INBUXA Admin {{version}}', { version: __APP_VERSION__ })}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -286,7 +286,7 @@ export function TopBar() {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<a href={sourceDownloadUrl()} target="_blank" rel="noopener noreferrer">
|
<a href={SOURCE_URL} target="_blank" rel="noopener noreferrer">
|
||||||
<FileCode className="mr-2 h-4 w-4" />
|
<FileCode className="mr-2 h-4 w-4" />
|
||||||
{t('source.menu', 'Source code (AGPL-3.0)')}
|
{t('source.menu', 'Source code (AGPL-3.0)')}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getBasePath } from '@/lib/basePath';
|
/**
|
||||||
|
* Where to point someone who wants this interface's source.
|
||||||
/** Where this build's own source is: written next to the app at build time (see source-archive.ts). */
|
*
|
||||||
export function sourceDownloadUrl(): string {
|
* INBUXA Admin is a modified Stalwart web interface, so the AGPL's offer is
|
||||||
return `${getBasePath()}/source.tar.gz`;
|
* this fork. The version shown beside the link names the build, which is what
|
||||||
}
|
* makes the offer something a person can act on.
|
||||||
|
*/
|
||||||
|
export const SOURCE_URL = 'https://github.com/inbuxa/inbuxa-admin';
|
||||||
|
|||||||
Vendored
-1
@@ -22,4 +22,3 @@ interface ImportMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare const __APP_VERSION__: string;
|
declare const __APP_VERSION__: string;
|
||||||
declare const __SOURCE_ID__: string;
|
|
||||||
|
|||||||
+1
-6
@@ -9,18 +9,13 @@ import { version } from './package.json' with { type: 'json' }
|
|||||||
// INBUXA's own dated version lives apart from package.json, whose version
|
// INBUXA's own dated version lives apart from package.json, whose version
|
||||||
// follows upstream WebUI so its bumps merge without conflicts.
|
// follows upstream WebUI so its bumps merge without conflicts.
|
||||||
import inbuxa from './inbuxa-version.json' with { type: 'json' }
|
import inbuxa from './inbuxa-version.json' with { type: 'json' }
|
||||||
import { sourceArchive, sourceIdentity } from './source-archive'
|
|
||||||
|
|
||||||
const source = sourceIdentity(import.meta.dirname)
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './',
|
base: './',
|
||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(`${inbuxa.version} (base ${version})`),
|
__APP_VERSION__: JSON.stringify(`${inbuxa.version} (base ${version})`),
|
||||||
// The tree this build came from; source.tar.gz next to the app holds it.
|
|
||||||
__SOURCE_ID__: JSON.stringify(source.id),
|
|
||||||
},
|
},
|
||||||
plugins: [react(), tailwindcss(), sourceArchive(import.meta.dirname, 'inbuxa-admin', source)],
|
plugins: [react(), tailwindcss()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(import.meta.dirname, './src'),
|
'@': path.resolve(import.meta.dirname, './src'),
|
||||||
|
|||||||
Reference in New Issue
Block a user