نحوه ساخت باینری های اجرایی چند پلتفرمی در Node.js با SEA، Rollup، Docker و GitHub

Summarize this content to 400 words in Persian Lang
نام من سرگئی است و نویسنده dclint، یک ابزار CLI برای پر کردن و قالب بندی فایل های Docker Compose هستم.
در این مقاله، نحوه تبدیل ابزار Node.js CLI را به یک ابزار انعطاف پذیر نشان خواهم داد که:
به عنوان یک باینری مستقل کار می کند، بنابراین نیازی به نصب Node.js ندارد.
از چندین معماری (arm64/amd64) و سیستم عامل (Alpine/Ubuntu) پشتیبانی می کند.
می تواند با استفاده از تصاویر Docker در خطوط لوله CI/CD ادغام شود.
ما مراحل کلیدی را پوشش خواهیم داد: استفاده از Node.js Single Executable Applications (SEA)، راهاندازی Rollup برای بستهبندی، ساخت تصاویر Docker بهینهسازی شده، و خودکار کردن فرآیند انتشار با GitHub Actions.
کمی زمینه
دیکلینت در TypeScript نوشته شده است زیرا این زبانی است که من با آن راحت تر هستم و مدل استفاده ای که در ابتدا در ذهن داشتم بسیار ساده بود:
از آنجایی که با فایلهای Docker Compose سر و کار داریم، Docker قبلاً نصب شده است. بنابراین توزیع ابزار به عنوان یک تصویر داکر این سوال را حل می کند که به کدام زبان نوشته شده است، زیرا داکر تنها وابستگی است. اما برای پروژه های Node.js، کاربران می توانند آن را از طریق آن نیز اجرا کنند npx.
با این حال (و این زیبایی منبع باز است)، یکی از کاربران روش دیگری را پیشنهاد کرد:
در مورد من، ما ابزارها را در تصاویر تخصصی جمعآوری میکنیم که به طور خاص با لایههای جمعشده میسازیم تا اجراکنندههای CI/CD ما نیازی به ذخیره لایههای زیادی نداشته باشند و به راحتی بتوانند ابزارهای مورد نظر را با کمترین اندازه تصویر کش کنند.»- آدام لیزکای در بحث GitHub
و این باعث شد به فکر ایجاد یک نسخه اجرایی از ابزارم باشم که اصلاً به Node.js وابسته نباشد.
بنابراین اهداف من این بود:
یک فرآیند ساخت واضح و سرراست.
یک باینری تا حد امکان کوچک.
سازگاری با حداقل اوبونتو و آلپاین.
پشتیبانی از هر دو معماری arm64 و amd64.
چه گزینه هایی دارم
چندین ابزار برای ایجاد باینری های مستقل وجود دارد:
هر دو ابزار با مستندات و مثالهای استفاده خوب ارائه میشوند. با این حال، در مورد من، همه چیز آنطور که می خواستم کار نکرد.
اگرچه نسبتاً اخیراً در Node.js 21 API خود را برای ایجاد برنامه های اجرایی منفرد معرفی کردند
در حال حاضر، این ویژگی در مرحله 1.1 است، یعنی در حال حاضر “تجربی. توسعه فعال.» اما من از کشف رویکردهای جدید لذت می برم، بنابراین تصمیم گرفتم آن را امتحان کنم.
در ادامه نحوه تنظیم آن را توضیح خواهم داد. اگر ترجیح می دهید مستقیماً وارد کد شوید، مخزن را بررسی کنید، و فراموش نکنید که اگر پروژه را دوست دارید، یک ستاره بگذارید!
Single Executable Applications API
به طور کلی این یک Node.js API است که به شما امکان می دهد برنامه خود را در یک فایل اجرایی بسته بندی کنید.
این ویژگی اجازه می دهد تا یک برنامه Node.js را به راحتی در سیستمی که Node.js نصب نشده است، توزیع کند.ویژگی برنامه اجرایی منفرد در حال حاضر تنها از اجرای یک اسکریپت جاسازی شده با استفاده از سیستم ماژول CommonJS پشتیبانی می کند.مستندات Node.js
مستندات عالی هستند و راهنمای گام به گام نحوه ایجاد یک فایل اجرایی را ارائه می دهند.
برای ساده کردن فرآیند، یک پوسته اسکریپت به نام ایجاد کردم generate-sea.sh. این اسکریپت مدیریت و اجرای دستورات لازم را در محیط های مختلف آسان می کند.
این اسکریپت است:
#!/bin/sh
# Checking that the path to the generation file is passed as an argument
if [ -z “$1” ]; then
echo “Usage: $0 ”
exit 1
fi
GENERATION_PATH=”$1″
# Generate binary
rm -rf “$GENERATION_PATH” && rm -rf sea-prep.blob && \
mkdir -p “$(dirname “$GENERATION_PATH”)” && \
node –experimental-sea-config sea-config.json && \
cp “$(command -v node)” “$GENERATION_PATH” && \
npx -y postject “$GENERATION_PATH” NODE_SEA_BLOB sea-prep.blob –sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
برای تولید باینری اجرایی، به سادگی اسکریپت را اجرا کنید و مسیر خروجی را مشخص کنید، به عنوان مثال:
./scripts/generate-sea.sh ./bin/dclint
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
همانطور که در مستندات ذکر شد، SEA فقط با یک تک کار می کند اسکریپت جاسازی شده با استفاده از سیستم ماژول CommonJS. بنابراین، برای انجام این کار، به یک باندلر نیاز دارید تا پروژه خود را در یک فایل CommonJS، شامل تمام وابستگیها از node_modules.
جمع آوری
من انتخاب کردم جمع آوری به عنوان باندلر این پروژه هنگام کامپایل کد در یک فایل، پشتیبانی از باندلر ضروری است درخت تکان دادن (حذف کدهای استفاده نشده). Rollup این قابلیت را به طور پیش فرض فعال کرده است.
Rollup یک بسته ماژول برای جاوا اسکریپت است که قطعات کوچکی از کد را در چیزی بزرگتر و پیچیده تر مانند کتابخانه یا برنامه کامپایل می کند.اسناد جمع آوری
برای رسیدن به نتیجه دلخواه، پیکربندی زیر را به Rollup اضافه کردم:
export default {
…baseConfig(‘pkg’, false, false), // Import a shared base config
input: ‘src/cli/cli.ts’,
output: {
file: ‘pkg/dclint.cjs’,
format: ‘cjs’,
inlineDynamicImports: true,
exports: ‘auto’,
},
context: ‘globalThis’,
};
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پیکربندی پایه مشترک، فایلهای TypeScript، JSON و سایر پیکربندیهای خاص پروژه را مدیریت میکند.
بر خلاف سایر تنظیمات ساخت، در اینجا تفاوت وجود دارد:
inlineDynamicImports: درست است – تمام منطق در یک فایل واحد جمع می شود، حتی اگر کد از واردات پویا استفاده کند.
فرمت: 'cjs' – فرمت باندل خروجی است CommonJS.
خیر external میدان – همه وابستگی ها در یک فایل همراه هستند.
نتیجه یک فایل جاوا اسکریپت 10 مگابایتی بود. پس از ایجاد باینری با SEA، حجم فایل به 100 مگابایت افزایش یافت. این برای یک ابزار نسبتا ساده بسیار بزرگ است، اما برای من خوب است.
و اکنون در نهایت کاملاً خودکفا شده است. یا هست؟
داکر
از آنجایی که SEA به صورت بومی از ساختن برای پلتفرمها و معماریهای مختلف پشتیبانی نمیکند و به محیطی که در آن اجرا میشود متکی است – داکر برای ساختهای چند پلتفرمی ضروری است.
Docker یک پلت فرم باز برای توسعه، حمل و نقل و اجرای برنامه ها است.Docker توانایی بسته بندی و اجرای یک برنامه را در یک محیط کاملاً ایزوله به نام کانتینر فراهم می کند.مستندات داکر
با Docker باینری تولید کنید
بنابراین در مورد من generate-sea.sh اسکریپت باید در همان محیطی که باینری در نظر گرفته شده است اجرا شود.
به عنوان مثال، برای ساختن یک باینری برای اوبونتو (arm64)، می توانم از دستور زیر استفاده کنم:
docker run –rm –platform linux/arm64 -v “$PWD”:/app -w /app node:20.18.0-bullseye ./scripts/generate-sea.sh ./sea/dclint-bullseye-arm64
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
توضیح:
–platform linux/arm64 معماری هدف را برای ساخت مشخص می کند.
node:20.18.0-bullseye یک تصویر Docker Node.js است که با اوبونتو سازگار است.
ایجاد تصویر داکر
علاوه بر تولید باینری ها، ابزار به صورت یک تصویر Docker توزیع می شود که نیاز به یک Dockerfile برای ساخت ظرف نهایی من از a استفاده می کنم ساخت چند مرحله ای برای به حداقل رساندن اندازه تصویر نهایی
در مرحله اول، باینری را با استفاده از generate-sea.sh اسکریپتو در مرحله آخر باینری تولید شده را کپی می کند و وابستگی های غیر ضروری را پشت سر می گذارد.
برای مرحله آخر از دو نوع تصویر استفاده می کنم: آلپاین و خراش.
Alpine یک تصویر پایه حداقل (~ 5 مگابایت)، ایده آل برای برنامه هایی است که به فضای کمی و امنیت بیشتر نیاز دارند. Alpine در Docker HubScratch یک تصویر پایه خالی برای کانتینرهای بسیار سبک است که برای فایل های اجرایی مستقل با حداقل وابستگی مناسب است. روی داکر هاب خراش دهید
مثال Dockerfile:
# First stage (builder)
# ————-
FROM node:20.18.0-alpine3.19 AS builder
# Create working directory
WORKDIR /dclint
# Copy package.json and install dependencies
COPY package*.json ./
RUN npm ci
# Copy the rest of the project
COPY . .
# Build the binary with Rollup and SEA script
RUN npm run build:pkg && ./scripts/generate-sea.sh /bin/dclint
# Final stage (alpine)
# ————-
FROM alpine:3.19 AS alpine-version
# Suppress experimental warnings
ENV NODE_NO_WARNINGS=1
# Copy the binary from the builder stage
COPY –from=builder /bin/dclint /bin/dclint
# Create working directory
WORKDIR /app
# Define the entry point
ENTRYPOINT [“/bin/dclint”]
# Final stage (scratch)
# ————-
FROM scratch AS scratch-version
# Suppress experimental warnings
ENV NODE_NO_WARNINGS=1
# Copy the binary from the builder stage
COPY –from=builder /bin/dclint /bin/dclint
# Create working directory
WORKDIR /app
# Define the entry point
ENTRYPOINT [“/bin/dclint”]
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
رسیدگی به وابستگی های کتابخانه
اما اجرای کانتینر از تصویر تولید شده باعث ایجاد خطاهایی مانند زیر می شود:
Error loading shared library libstdc++.so.6: No such file or directory (needed by /bin/dclint)
Error relocating /bin/dclint: _ZNSt7__cxx1119basic_ostringstreamIcSt11char_traitsIcESaIcEEC1Ev: symbol not found
…
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این به این دلیل اتفاق می افتد که، حتی اگر Node.js در باینری بسته شده است، همچنان به آن نیاز دارد libstdc++ کتابخانه، همانطور که توسط ldd /bin/dclint دستور:
ldd /bin/dclint
/lib/ld-musl-aarch64.so.1 (0xffffaeac8000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xffff9fe00000)
libc.musl-aarch64.so.1 => /lib/ld-musl-aarch64.so.1 (0xffffaeac8000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xffffaea97000)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
البته میتوانید این وابستگیها را به صورت زیر در مرحله نهایی کپی کنید:
# Copy library dependencies
COPY –from=builder /lib/ld-musl-aarch64.so.1 /lib/ld-musl-aarch64.so.1
COPY –from=builder /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
COPY –from=builder /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
با این حال، از آنجایی که وابستگی ها در معماری ها متفاوت است (به عنوان مثال، arm64 در مقابل amd64)، من از خروجی استفاده می کنم ldd /bin/dclint برای شناسایی وابستگی ها به صورت پویا، آنها را در یک پوشه جداگانه کپی کنید و سپس در مرحله نهایی قرار دهید:
# Collect platform-specific dependencies
RUN mkdir -p /dependencies/lib /dependencies/usr/lib && \
ldd /bin/dclint | awk ‘{print $3}’ | grep -vE ‘^$’ | while read -r lib; do \
if [ -f “$lib” ]; then \
if [ “${lib#/usr/lib/}” != “$lib” ]; then \
cp “$lib” /dependencies/usr/lib/; \
elif [ “${lib#/lib/}” != “$lib” ]; then \
cp “$lib” /dependencies/lib/; \
fi; \
fi; \
done
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
با این رویکرد، فینال Dockerfile به نظر می رسد این است:
# First stage (builder)
# ————-
FROM node:20.18.0-alpine3.19 AS builder
WORKDIR /dclint
COPY package*.json ./
RUN npm ci
COPY . .
# SEA Builder
RUN npm run build:pkg && ./scripts/generate-sea.sh /bin/dclint
# Collect platform-specific dependencies
SHELL [“/bin/ash”, “-o”, “pipefail”, “-c”]
RUN mkdir -p /dependencies/lib /dependencies/usr/lib && \
ldd /bin/dclint | awk ‘{print $3}’ | grep -vE ‘^$’ | while read -r lib; do \
if [ -f “$lib” ]; then \
if [ “${lib#/usr/lib/}” != “$lib” ]; then \
cp “$lib” /dependencies/usr/lib/; \
elif [ “${lib#/lib/}” != “$lib” ]; then \
cp “$lib” /dependencies/lib/; \
fi; \
fi; \
done
# Final stage (alpine)
# ————-
FROM alpine:3.19 AS alpine-version
ENV NODE_NO_WARNINGS=1
# Install c++ dependencies
RUN apk update && apk upgrade && \
apk add –no-cache \
libstdc++=~13.2 \
&& rm -rf /tmp/* /var/cache/apk/*
COPY –from=builder /bin/dclint /bin/dclint
WORKDIR /app
ENTRYPOINT [“/bin/dclint”]
# Final stage (scratch)
# ————-
FROM scratch AS scratch-version
ENV NODE_NO_WARNINGS=1
# Copy dependencies
COPY –from=builder /dependencies/lib /lib
COPY –from=builder /dependencies/usr/lib /usr/lib
# Copy binary
COPY –from=builder /bin/dclint /bin/dclint
WORKDIR /app
ENTRYPOINT [“/bin/dclint”]
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
Dockerfile را در GitHub مشاهده کنید
GitHub
با خط لوله GitHub من می خواستم به دو هدف برسم:
منتشر کنید alpine و scratch نسخه ها (پشتیبانی از هر دو amd64 و arm64) به داکر هاب.
باینری های اجرایی برای Alpine/Ubuntu (همچنین amd64 و arm64) به عنوان دارایی های نسخه های GitHub.
انتشار در Docker Hub
برای انتشار تصاویر در Docker Hub، از docker/build-push-action@v6 استفاده میکنم، جایی که مشخص میکنم:
هدف: کدام تصویر نهایی منتشر شود.
پلت فرم: پلتفرم هایی برای ساختن.
برچسب ها: برچسب هایی که تصویر زیر آنها منتشر می شود.
این عمل دو بار فراخوانی می شود – برای alpine نسخه و برای scratch نسخه در اینجا یک مثال برای scratch نسخه:
jobs:
release:
runs-on: ubuntu-latest
steps:
– …
– name: Build and push Scratch version
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/dclint:latest
${{ secrets.DOCKERHUB_USERNAME }}/dclint:${{ env.BUILD_VERSION }}
target: scratch-version
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ساخت باینری
برای ساخت های باینری، من استفاده می کنم ماتریس می سازد (راهنمای ماتریس اقدامات GitHub) در گردش کار. این امکان مدیریت همزمان پلتفرم ها و معماری های مختلف را فراهم می کند:
jobs:
build_sea:
runs-on: ubuntu-latest
strategy:
matrix:
os: [alpine, bullseye]
arch: [amd64, arm64]
steps:
– …
– name: Build binary
run: |
docker run –rm –platform linux/${{ matrix.arch }} -v “$PWD”:/app -w /app node:20.18.0-${{ matrix.os }} ./scripts/generate-sea.sh ./sea/dclint-${{ matrix.os }}-${{ matrix.arch }}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
شما می توانید خط لوله کامل را در اینجا مشاهده کنید:فایل گردش کار GitHub
افزودن باینری به نسخه ها
باینری ها به طور خودکار از طریق به نسخه ها اضافه می شوند semantic-release، اگرچه می توان آن را به روش های دیگری نیز انجام داد.
در اینجا بخشی از release.config.js مسئول پیوست کردن فایل ها:
export default {
…
plugins: [
…
[
‘@semantic-release/github’,
{
assets: [
{
path: ‘README.md’,
label: ‘Documentation’,
},
{
path: ‘CHANGELOG.md’,
label: ‘Changelog’,
},
{
path: ‘sea/dclint-alpine-amd64’,
label: ‘DClint Alpine Linux Binary (amd64)’,
},
{
path: ‘sea/dclint-bullseye-amd64’,
label: ‘DClint Bullseye Linux Binary (amd64)’,
},
{
path: ‘sea/dclint-alpine-arm64’,
label: ‘DClint Alpine Linux Binary (arm64)’,
},
{
path: ‘sea/dclint-bullseye-arm64’,
label: ‘DClint Bullseye Linux Binary (arm64)’,
},
],
},
],
],
};
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
Release.config.js را در GitHub مشاهده کنید
افکار نهایی
در حین کار بر روی dclint، با کارهایی روبرو شدم که در ابتدا ساده به نظر می رسیدند اما به سرعت تبدیل به چالش های جالبی شدند.
این چالش ها به من تجربیات ارزشمندی داد و به ساختن آن کمک کرد dclint ابزار کاربردی تر: بدون Node.js اجرا می شود، از چندین معماری پشتیبانی می کند و می تواند به راحتی از طریق Docker یا به عنوان یک فایل مستقل نصب شود.
پس از تمام این بهینه سازی ها، من توانستم اندازه تصاویر Docker را به میزان قابل توجهی کاهش دهم:
اندازه فشرده تصویر Docker مبتنی بر آلپاین کاهش یافت 93 مگابایت به 43 مگابایت.
برای نسخه جدید مبتنی بر خراش، اندازه است 38 مگابایت.
اگر می خواهید ببینید که چگونه همه کار می کند، مخزن را بررسی کنید. من از ستاره های شما و هر پیشنهادی برای بهبود قدردانی می کنم.
اگر این مقاله را دوست داشتید، می توانید با پی پال از من حمایت کنید یا من را دنبال کنید:
نام من سرگئی است و نویسنده dclint، یک ابزار CLI برای پر کردن و قالب بندی فایل های Docker Compose هستم.
در این مقاله، نحوه تبدیل ابزار Node.js CLI را به یک ابزار انعطاف پذیر نشان خواهم داد که:
- به عنوان یک باینری مستقل کار می کند، بنابراین نیازی به نصب Node.js ندارد.
- از چندین معماری (arm64/amd64) و سیستم عامل (Alpine/Ubuntu) پشتیبانی می کند.
- می تواند با استفاده از تصاویر Docker در خطوط لوله CI/CD ادغام شود.
ما مراحل کلیدی را پوشش خواهیم داد: استفاده از Node.js Single Executable Applications (SEA)، راهاندازی Rollup برای بستهبندی، ساخت تصاویر Docker بهینهسازی شده، و خودکار کردن فرآیند انتشار با GitHub Actions.
کمی زمینه
دیکلینت در TypeScript نوشته شده است زیرا این زبانی است که من با آن راحت تر هستم و مدل استفاده ای که در ابتدا در ذهن داشتم بسیار ساده بود:
از آنجایی که با فایلهای Docker Compose سر و کار داریم، Docker قبلاً نصب شده است. بنابراین توزیع ابزار به عنوان یک تصویر داکر این سوال را حل می کند که به کدام زبان نوشته شده است، زیرا داکر تنها وابستگی است. اما برای پروژه های Node.js، کاربران می توانند آن را از طریق آن نیز اجرا کنند npx
.
با این حال (و این زیبایی منبع باز است)، یکی از کاربران روش دیگری را پیشنهاد کرد:
در مورد من، ما ابزارها را در تصاویر تخصصی جمعآوری میکنیم که به طور خاص با لایههای جمعشده میسازیم تا اجراکنندههای CI/CD ما نیازی به ذخیره لایههای زیادی نداشته باشند و به راحتی بتوانند ابزارهای مورد نظر را با کمترین اندازه تصویر کش کنند.»
– آدام لیزکای در بحث GitHub
و این باعث شد به فکر ایجاد یک نسخه اجرایی از ابزارم باشم که اصلاً به Node.js وابسته نباشد.
بنابراین اهداف من این بود:
- یک فرآیند ساخت واضح و سرراست.
- یک باینری تا حد امکان کوچک.
- سازگاری با حداقل اوبونتو و آلپاین.
- پشتیبانی از هر دو معماری arm64 و amd64.
چه گزینه هایی دارم
چندین ابزار برای ایجاد باینری های مستقل وجود دارد:
هر دو ابزار با مستندات و مثالهای استفاده خوب ارائه میشوند. با این حال، در مورد من، همه چیز آنطور که می خواستم کار نکرد.
اگرچه نسبتاً اخیراً در Node.js 21 API خود را برای ایجاد برنامه های اجرایی منفرد معرفی کردند
در حال حاضر، این ویژگی در مرحله 1.1 است، یعنی در حال حاضر “تجربی. توسعه فعال.» اما من از کشف رویکردهای جدید لذت می برم، بنابراین تصمیم گرفتم آن را امتحان کنم.
در ادامه نحوه تنظیم آن را توضیح خواهم داد. اگر ترجیح می دهید مستقیماً وارد کد شوید، مخزن را بررسی کنید، و فراموش نکنید که اگر پروژه را دوست دارید، یک ستاره بگذارید!
Single Executable Applications API
به طور کلی این یک Node.js API است که به شما امکان می دهد برنامه خود را در یک فایل اجرایی بسته بندی کنید.
این ویژگی اجازه می دهد تا یک برنامه Node.js را به راحتی در سیستمی که Node.js نصب نشده است، توزیع کند.
ویژگی برنامه اجرایی منفرد در حال حاضر تنها از اجرای یک اسکریپت جاسازی شده با استفاده از سیستم ماژول CommonJS پشتیبانی می کند.مستندات Node.js
مستندات عالی هستند و راهنمای گام به گام نحوه ایجاد یک فایل اجرایی را ارائه می دهند.
برای ساده کردن فرآیند، یک پوسته اسکریپت به نام ایجاد کردم generate-sea.sh
. این اسکریپت مدیریت و اجرای دستورات لازم را در محیط های مختلف آسان می کند.
این اسکریپت است:
#!/bin/sh
# Checking that the path to the generation file is passed as an argument
if [ -z "$1" ]; then
echo "Usage: $0 "
exit 1
fi
GENERATION_PATH="$1"
# Generate binary
rm -rf "$GENERATION_PATH" && rm -rf sea-prep.blob && \
mkdir -p "$(dirname "$GENERATION_PATH")" && \
node --experimental-sea-config sea-config.json && \
cp "$(command -v node)" "$GENERATION_PATH" && \
npx -y postject "$GENERATION_PATH" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
برای تولید باینری اجرایی، به سادگی اسکریپت را اجرا کنید و مسیر خروجی را مشخص کنید، به عنوان مثال:
./scripts/generate-sea.sh ./bin/dclint
همانطور که در مستندات ذکر شد، SEA فقط با یک تک کار می کند اسکریپت جاسازی شده با استفاده از سیستم ماژول CommonJS. بنابراین، برای انجام این کار، به یک باندلر نیاز دارید تا پروژه خود را در یک فایل CommonJS، شامل تمام وابستگیها از node_modules
.
جمع آوری
من انتخاب کردم جمع آوری به عنوان باندلر این پروژه هنگام کامپایل کد در یک فایل، پشتیبانی از باندلر ضروری است درخت تکان دادن (حذف کدهای استفاده نشده). Rollup این قابلیت را به طور پیش فرض فعال کرده است.
Rollup یک بسته ماژول برای جاوا اسکریپت است که قطعات کوچکی از کد را در چیزی بزرگتر و پیچیده تر مانند کتابخانه یا برنامه کامپایل می کند.
اسناد جمع آوری
برای رسیدن به نتیجه دلخواه، پیکربندی زیر را به Rollup اضافه کردم:
export default {
...baseConfig('pkg', false, false), // Import a shared base config
input: 'src/cli/cli.ts',
output: {
file: 'pkg/dclint.cjs',
format: 'cjs',
inlineDynamicImports: true,
exports: 'auto',
},
context: 'globalThis',
};
پیکربندی پایه مشترک، فایلهای TypeScript، JSON و سایر پیکربندیهای خاص پروژه را مدیریت میکند.
بر خلاف سایر تنظیمات ساخت، در اینجا تفاوت وجود دارد:
- inlineDynamicImports: درست است – تمام منطق در یک فایل واحد جمع می شود، حتی اگر کد از واردات پویا استفاده کند.
- فرمت: 'cjs' – فرمت باندل خروجی است CommonJS.
-
خیر
external
میدان – همه وابستگی ها در یک فایل همراه هستند.
نتیجه یک فایل جاوا اسکریپت 10 مگابایتی بود. پس از ایجاد باینری با SEA، حجم فایل به 100 مگابایت افزایش یافت. این برای یک ابزار نسبتا ساده بسیار بزرگ است، اما برای من خوب است.
و اکنون در نهایت کاملاً خودکفا شده است. یا هست؟
داکر
از آنجایی که SEA به صورت بومی از ساختن برای پلتفرمها و معماریهای مختلف پشتیبانی نمیکند و به محیطی که در آن اجرا میشود متکی است – داکر برای ساختهای چند پلتفرمی ضروری است.
Docker یک پلت فرم باز برای توسعه، حمل و نقل و اجرای برنامه ها است.
Docker توانایی بسته بندی و اجرای یک برنامه را در یک محیط کاملاً ایزوله به نام کانتینر فراهم می کند.مستندات داکر
با Docker باینری تولید کنید
بنابراین در مورد من generate-sea.sh
اسکریپت باید در همان محیطی که باینری در نظر گرفته شده است اجرا شود.
به عنوان مثال، برای ساختن یک باینری برای اوبونتو (arm64)، می توانم از دستور زیر استفاده کنم:
docker run --rm --platform linux/arm64 -v "$PWD":/app -w /app node:20.18.0-bullseye ./scripts/generate-sea.sh ./sea/dclint-bullseye-arm64
توضیح:
-
--platform linux/arm64
معماری هدف را برای ساخت مشخص می کند. -
node:20.18.0-bullseye
یک تصویر Docker Node.js است که با اوبونتو سازگار است.
ایجاد تصویر داکر
علاوه بر تولید باینری ها، ابزار به صورت یک تصویر Docker توزیع می شود که نیاز به یک Dockerfile
برای ساخت ظرف نهایی من از a استفاده می کنم ساخت چند مرحله ای برای به حداقل رساندن اندازه تصویر نهایی
در مرحله اول، باینری را با استفاده از generate-sea.sh
اسکریپت
و در مرحله آخر باینری تولید شده را کپی می کند و وابستگی های غیر ضروری را پشت سر می گذارد.
برای مرحله آخر از دو نوع تصویر استفاده می کنم: آلپاین و خراش.
Alpine یک تصویر پایه حداقل (~ 5 مگابایت)، ایده آل برای برنامه هایی است که به فضای کمی و امنیت بیشتر نیاز دارند. Alpine در Docker Hub
Scratch یک تصویر پایه خالی برای کانتینرهای بسیار سبک است که برای فایل های اجرایی مستقل با حداقل وابستگی مناسب است. روی داکر هاب خراش دهید
مثال Dockerfile:
# First stage (builder)
# -------------
FROM node:20.18.0-alpine3.19 AS builder
# Create working directory
WORKDIR /dclint
# Copy package.json and install dependencies
COPY package*.json ./
RUN npm ci
# Copy the rest of the project
COPY . .
# Build the binary with Rollup and SEA script
RUN npm run build:pkg && ./scripts/generate-sea.sh /bin/dclint
# Final stage (alpine)
# -------------
FROM alpine:3.19 AS alpine-version
# Suppress experimental warnings
ENV NODE_NO_WARNINGS=1
# Copy the binary from the builder stage
COPY --from=builder /bin/dclint /bin/dclint
# Create working directory
WORKDIR /app
# Define the entry point
ENTRYPOINT ["/bin/dclint"]
# Final stage (scratch)
# -------------
FROM scratch AS scratch-version
# Suppress experimental warnings
ENV NODE_NO_WARNINGS=1
# Copy the binary from the builder stage
COPY --from=builder /bin/dclint /bin/dclint
# Create working directory
WORKDIR /app
# Define the entry point
ENTRYPOINT ["/bin/dclint"]
رسیدگی به وابستگی های کتابخانه
اما اجرای کانتینر از تصویر تولید شده باعث ایجاد خطاهایی مانند زیر می شود:
Error loading shared library libstdc++.so.6: No such file or directory (needed by /bin/dclint)
Error relocating /bin/dclint: _ZNSt7__cxx1119basic_ostringstreamIcSt11char_traitsIcESaIcEEC1Ev: symbol not found
...
این به این دلیل اتفاق می افتد که، حتی اگر Node.js در باینری بسته شده است، همچنان به آن نیاز دارد libstdc++
کتابخانه، همانطور که توسط ldd /bin/dclint
دستور:
ldd /bin/dclint
/lib/ld-musl-aarch64.so.1 (0xffffaeac8000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xffff9fe00000)
libc.musl-aarch64.so.1 => /lib/ld-musl-aarch64.so.1 (0xffffaeac8000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xffffaea97000)
البته میتوانید این وابستگیها را به صورت زیر در مرحله نهایی کپی کنید:
# Copy library dependencies
COPY --from=builder /lib/ld-musl-aarch64.so.1 /lib/ld-musl-aarch64.so.1
COPY --from=builder /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
COPY --from=builder /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6
با این حال، از آنجایی که وابستگی ها در معماری ها متفاوت است (به عنوان مثال، arm64 در مقابل amd64)، من از خروجی استفاده می کنم ldd /bin/dclint
برای شناسایی وابستگی ها به صورت پویا، آنها را در یک پوشه جداگانه کپی کنید و سپس در مرحله نهایی قرار دهید:
# Collect platform-specific dependencies
RUN mkdir -p /dependencies/lib /dependencies/usr/lib && \
ldd /bin/dclint | awk '{print $3}' | grep -vE '^$' | while read -r lib; do \
if [ -f "$lib" ]; then \
if [ "${lib#/usr/lib/}" != "$lib" ]; then \
cp "$lib" /dependencies/usr/lib/; \
elif [ "${lib#/lib/}" != "$lib" ]; then \
cp "$lib" /dependencies/lib/; \
fi; \
fi; \
done
با این رویکرد، فینال Dockerfile
به نظر می رسد این است:
# First stage (builder)
# -------------
FROM node:20.18.0-alpine3.19 AS builder
WORKDIR /dclint
COPY package*.json ./
RUN npm ci
COPY . .
# SEA Builder
RUN npm run build:pkg && ./scripts/generate-sea.sh /bin/dclint
# Collect platform-specific dependencies
SHELL ["/bin/ash", "-o", "pipefail", "-c"]
RUN mkdir -p /dependencies/lib /dependencies/usr/lib && \
ldd /bin/dclint | awk '{print $3}' | grep -vE '^$' | while read -r lib; do \
if [ -f "$lib" ]; then \
if [ "${lib#/usr/lib/}" != "$lib" ]; then \
cp "$lib" /dependencies/usr/lib/; \
elif [ "${lib#/lib/}" != "$lib" ]; then \
cp "$lib" /dependencies/lib/; \
fi; \
fi; \
done
# Final stage (alpine)
# -------------
FROM alpine:3.19 AS alpine-version
ENV NODE_NO_WARNINGS=1
# Install c++ dependencies
RUN apk update && apk upgrade && \
apk add --no-cache \
libstdc++=~13.2 \
&& rm -rf /tmp/* /var/cache/apk/*
COPY --from=builder /bin/dclint /bin/dclint
WORKDIR /app
ENTRYPOINT ["/bin/dclint"]
# Final stage (scratch)
# -------------
FROM scratch AS scratch-version
ENV NODE_NO_WARNINGS=1
# Copy dependencies
COPY --from=builder /dependencies/lib /lib
COPY --from=builder /dependencies/usr/lib /usr/lib
# Copy binary
COPY --from=builder /bin/dclint /bin/dclint
WORKDIR /app
ENTRYPOINT ["/bin/dclint"]
Dockerfile را در GitHub مشاهده کنید
GitHub
با خط لوله GitHub من می خواستم به دو هدف برسم:
- منتشر کنید
alpine
وscratch
نسخه ها (پشتیبانی از هر دوamd64
وarm64
) به داکر هاب. - باینری های اجرایی برای Alpine/Ubuntu (همچنین
amd64
وarm64
) به عنوان دارایی های نسخه های GitHub.
انتشار در Docker Hub
برای انتشار تصاویر در Docker Hub، از docker/build-push-action@v6 استفاده میکنم، جایی که مشخص میکنم:
- هدف: کدام تصویر نهایی منتشر شود.
- پلت فرم: پلتفرم هایی برای ساختن.
- برچسب ها: برچسب هایی که تصویر زیر آنها منتشر می شود.
این عمل دو بار فراخوانی می شود – برای alpine
نسخه و برای scratch
نسخه در اینجا یک مثال برای scratch
نسخه:
jobs:
release:
runs-on: ubuntu-latest
steps:
- ...
- name: Build and push Scratch version
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/dclint:latest
${{ secrets.DOCKERHUB_USERNAME }}/dclint:${{ env.BUILD_VERSION }}
target: scratch-version
ساخت باینری
برای ساخت های باینری، من استفاده می کنم ماتریس می سازد (راهنمای ماتریس اقدامات GitHub) در گردش کار. این امکان مدیریت همزمان پلتفرم ها و معماری های مختلف را فراهم می کند:
jobs:
build_sea:
runs-on: ubuntu-latest
strategy:
matrix:
os: [alpine, bullseye]
arch: [amd64, arm64]
steps:
- ...
- name: Build binary
run: |
docker run --rm --platform linux/${{ matrix.arch }} -v "$PWD":/app -w /app node:20.18.0-${{ matrix.os }} ./scripts/generate-sea.sh ./sea/dclint-${{ matrix.os }}-${{ matrix.arch }}
شما می توانید خط لوله کامل را در اینجا مشاهده کنید:
فایل گردش کار GitHub
افزودن باینری به نسخه ها
باینری ها به طور خودکار از طریق به نسخه ها اضافه می شوند semantic-release
، اگرچه می توان آن را به روش های دیگری نیز انجام داد.
در اینجا بخشی از release.config.js
مسئول پیوست کردن فایل ها:
export default {
...
plugins: [
...
[
'@semantic-release/github',
{
assets: [
{
path: 'README.md',
label: 'Documentation',
},
{
path: 'CHANGELOG.md',
label: 'Changelog',
},
{
path: 'sea/dclint-alpine-amd64',
label: 'DClint Alpine Linux Binary (amd64)',
},
{
path: 'sea/dclint-bullseye-amd64',
label: 'DClint Bullseye Linux Binary (amd64)',
},
{
path: 'sea/dclint-alpine-arm64',
label: 'DClint Alpine Linux Binary (arm64)',
},
{
path: 'sea/dclint-bullseye-arm64',
label: 'DClint Bullseye Linux Binary (arm64)',
},
],
},
],
],
};
Release.config.js را در GitHub مشاهده کنید
افکار نهایی
در حین کار بر روی dclint
، با کارهایی روبرو شدم که در ابتدا ساده به نظر می رسیدند اما به سرعت تبدیل به چالش های جالبی شدند.
این چالش ها به من تجربیات ارزشمندی داد و به ساختن آن کمک کرد dclint
ابزار کاربردی تر: بدون Node.js اجرا می شود، از چندین معماری پشتیبانی می کند و می تواند به راحتی از طریق Docker یا به عنوان یک فایل مستقل نصب شود.
پس از تمام این بهینه سازی ها، من توانستم اندازه تصاویر Docker را به میزان قابل توجهی کاهش دهم:
- اندازه فشرده تصویر Docker مبتنی بر آلپاین کاهش یافت 93 مگابایت به 43 مگابایت.
- برای نسخه جدید مبتنی بر خراش، اندازه است 38 مگابایت.
اگر می خواهید ببینید که چگونه همه کار می کند، مخزن را بررسی کنید. من از ستاره های شما و هر پیشنهادی برای بهبود قدردانی می کنم.
اگر این مقاله را دوست داشتید، می توانید با پی پال از من حمایت کنید یا من را دنبال کنید: