Skip to content

Commit

Permalink
feat: install esgleam on any platform (without curl)
Browse files Browse the repository at this point in the history
  • Loading branch information
Enderchief committed Jan 1, 2024
1 parent 2bf9218 commit b7badb3
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 13 deletions.
7 changes: 6 additions & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "esgleam"
version = "0.2.1"
version = "0.3.0"
gleam = ">= 0.32.0"

description = "esbuild for Gleam"
Expand All @@ -9,9 +9,14 @@ repository = { type = "github", user = "Enderchief", repo = "esgleam" }

target = "javascript"

[javascript]
typescript_declarations = true
# runtime = "deno"

[dependencies]
gleam_stdlib = "~> 0.34"
simplifile = "~> 1.1"
thoas = "~> 1.2"

[dev-dependencies]
gleeunit = "~> 1.0"
2 changes: 2 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ packages = [
{ name = "gleam_stdlib", version = "0.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" },
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
{ name = "simplifile", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "2B7070FB8617474A35651F6AA27046576615C14A4D97B62FA7C40C24C55A6C5C" },
{ name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" },
]

[requirements]
gleam_stdlib = { version = "~> 0.34" }
gleeunit = { version = "~> 1.0" }
simplifile = { version = "~> 1.1" }
thoas = { version = "~> 1.2" }
2 changes: 1 addition & 1 deletion src/esgleam.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ fn do_bundle(config: Config) {
|> string.join(with: " ")

let cmd =
string_builder.from_string("./priv/bin/esbuild ")
string_builder.from_string("./priv/package/bin/esbuild ")
|> append(entries)
|> append(" --bundle")
|> append(" --outdir=")
Expand Down
8 changes: 2 additions & 6 deletions src/esgleam/install.gleam
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import simplifile
import esgleam/internal.{exec_shell}

const cmd = "curl -fsSL https://esbuild.github.io/dl/latest | sh"
import esgleam/internal/fetch_esbuild

/// Installs `esbuild` (required to be run before building)
/// Run with `gleam run -m esgleam/install`
pub fn main() {
let assert Ok(_) = simplifile.create_directory_all("./priv/bin")
exec_shell(cmd, "./priv/bin")
fetch_esbuild.fetch()
}
17 changes: 17 additions & 0 deletions src/esgleam/internal/fetch_esbuild.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import gleam/io
import gleam/string
import esgleam/internal/platform

const base_url = "https://registry.npmjs.org/{#version}/latest"

@external(erlang, "ffi_esgleam", "do_fetch")
@external(javascript, "../../ffi_esgleam.mjs", "do_fetch")
fn do_fetch(url: String) -> Nil

pub fn fetch() {
let version = platform.get_package_name()
let url = string.replace(base_url, "{#version}", version)
io.println("Fetching esbuild from: " <> url)
do_fetch(url)
Nil
}
49 changes: 49 additions & 0 deletions src/esgleam/internal/platform.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
pub type OsName {
Win32
Linux
Darwin
Solaris
Sunos
Freebsd
Openbsd
}

pub type Arch {
X64
Ia32
Arm64
Arm
Ppc64
}

@external(erlang, "ffi_esgleam", "get_os")
@external(javascript, "../../ffi_esgleam.mjs", "get_os")
pub fn get_os() -> Result(OsName, Nil)

@external(erlang, "ffi_esgleam", "get_arch")
@external(javascript, "../../ffi_esgleam.mjs", "get_arch")
pub fn get_arch() -> Result(Arch, Nil)

pub fn get_package_name() {
let assert Ok(os) = get_os()
let assert Ok(arch) = get_arch()

let os_str = case os {
Win32 -> "win32"
Linux -> "linux"
Darwin -> "darwin"
Solaris | Sunos -> "sunos"
Freebsd -> "freebsd"
Openbsd -> "openbsd"
}

let arch_str = case arch {
X64 -> "x64"
Ia32 -> "ia32"
Arm64 -> "arm64"
Arm -> "arm"
Ppc64 -> "ppc64"
}

"@esbuild/" <> os_str <> "-" <> arch_str
}
66 changes: 66 additions & 0 deletions src/ffi_esgleam.erl
Original file line number Diff line number Diff line change
@@ -1 +1,67 @@
-module(ffi_esgleam).

-export([get_arch/0, get_os/0, do_fetch/1]).

-spec get_arch() -> {ok, x64 | ia32 | arm64 | arm | ppc64} | {err, string()}.
get_arch() ->
% #TODO: find out how to detect if it's not ia32/x64 for windows :P
case erlang:system_info(os_type) of
{unix, _} ->
[Arch_Raw, _] = string:split(erlang:system_info(system_architecture), "-"),
% based on the values that `uname -a` returns
case Arch_Raw of
"x86_64" ->
{ok, x64};
"amd64" ->
{ok, x64};
"i386" ->
{ok, ia32};
"i586" ->
{ok, ia32};
"i686" ->
{ok, ia32};
"aarch64" ->
{ok, arm64};
"armv7l" ->
{ok, arm};
"ppc64" ->
{ok, ppc64};
_ -> {err, Arch_Raw}
end;
{win32, _} ->
case erlang:system_info(wordsize) of
4 -> {ok, ia32};
8 -> {ok, x64}
end
end.

% list compiled from erlang otp source
-spec get_os() -> {ok, win32 | darwin | linux | solaris | freebsd | openbsd | dragonfly | irix64 | irix | posix} | {err, atom()}.
get_os() ->
case erlang:system_info(os_type) of
{win32, _} -> win32;
{unix, linux} -> {ok, linux};
{unix, darwin} -> {ok, darwin};
{unix, sunos} -> {ok, solaris};
{unix, sunos4} -> {ok, sunos}; % technically not solaris
{unix, freebsd} -> {ok, freebsd};
{unix, openbsd} -> {ok, openbsd};
{unix, Os} -> {err, Os}
end.

-spec do_fetch(string()) -> {}.
do_fetch(Url) ->
inets:start(),
ssl:start(),
{ok, {{_, 200, _}, _, Body}} = httpc:request(get, {Url, []}, [], []),
{ok, Res} = thoas:decode(Body),
#{<<"dist">> := #{<<"tarball">> := TarUrl}} = Res,
fetch_tarball(TarUrl)
.

-spec fetch_tarball(string()) -> {}.
fetch_tarball(Url) ->
io:fwrite("Fetching tarball from: ~s\n", [Url]),
{ok, {{_, 200, _}, _, Binary}} = httpc:request(get, {Url, []}, [], [{body_format, binary}]),
ok = erl_tar:extract({binary, Binary}, [compressed, verbose, {cwd, "./priv"}, {files, ["package/bin/esbuild"]}])
.
95 changes: 90 additions & 5 deletions src/ffi_esgleam.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,98 @@
import { exec, spawn } from "node:child_process";
import { exec, spawn } from 'node:child_process';
import { chmod, mkdir, writeFile } from 'node:fs/promises';
import * as process from 'node:process';
import {
Win32,
Linux,
Darwin,
Solaris,
Freebsd,
Openbsd,
Arm,
Arm64,
Ia32,
Ppc64,
X64,
// @ts-expect-error
} from './esgleam/internal/platform.mjs';
// @ts-expect-error
import { Ok, Error } from './gleam.mjs';

import { entries } from './streaming_tar.mjs';

export function exec_shell(command, cwd) {
if (typeof command === "string")
exec(command, { cwd, encoding: "utf-8" }, (_, stdout, stderr) => {
if (typeof command === 'string')
exec(command, { cwd, encoding: 'utf-8' }, (_, stdout, stderr) => {
console.log(stdout);
console.error(stderr);
});
else if (typeof command === "object") {
else if (typeof command === 'object') {
command = command.toArray();
spawn(command[0], command.slice(1), { cwd, stdio: "inherit" });
spawn(command[0], command.slice(1), { cwd, stdio: 'inherit' });
}
}

/** @type {Partial<Record<NodeJS.Platform, () => import('./esgleam/internal/platform.gleam').OsName$>>} */
const platform_map = {
darwin: () => new Darwin(),
freebsd: () => new Freebsd(),
linux: () => new Linux(),
openbsd: () => new Openbsd(),
sunos: () => new Solaris(),
win32: () => new Win32(),
};

export function get_os() {
const platform = process.platform;
const res = platform_map[platform]();
if (res) return new Ok(res);
return new Error(res);
}

/** @type {Partial<Record<NodeJS.Architecture, () => import('./esgleam/internal/platform.gleam').Arch$>>} */
const arch_map = {
arm: () => new Arm(),
arm64: () => new Arm64(),
ia32: () => new Ia32(),
ppc64: () => new Ppc64(),
x64: () => new X64(),
};

export function get_arch() {
const arch = process.arch;
const res = arch_map[arch]();
if (res) return new Ok(res);
return new Error(res);
}

/**
* @param {string} url
*/
export async function do_fetch(url) {
const info_res = await fetch(url);
if (!info_res.ok)
return console.error(
`Oh no. Something went wrong. Error fetching "${url}"\n${await info_res.text()}`
);
const content = await info_res.json();
const tarball_url = content.dist.tarball;
console.log(`Fetching tarball from: ${tarball_url}`);
const tarResp = await fetch(tarball_url);
const tarStream = tarResp.body.pipeThrough(new DecompressionStream('gzip'));

for await (const entry of entries(tarStream)) {
if (entry.name == 'package/bin/esbuild') {
console.log(entry.name);
try {
await mkdir('./priv/package/bin', { recursive: true });
} catch {}
await writeFile(
'./priv/package/bin/esbuild',
Buffer.from(await entry.arrayBuffer()),
{ encoding: 'binary' }
);
await chmod('./priv/package/bin/esbuild', 0o777);
break;
}
}
}
25 changes: 25 additions & 0 deletions src/streaming_tar.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
MIT License
Copyright (c) 2023 Zebulon Piasecki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

var o=class{promise;resolve;constructor(){this.promise=new Promise(e=>this.resolve=e)}};var c=class{#e;#r;#s;#t;bodyUsed=!1;constructor(e,t,r,s){this.#e=e,this.#r=t,this.#s=s,this.#t=r.length>0?r:void 0}get name(){return this.#e.name}get fileSize(){return this.#e.fileSize}get#n(){let e=Math.ceil(this.#e.fileSize/512)*512;return new ReadableStream({start:()=>{if(this.bodyUsed)throw new Error("Body already used");this.bodyUsed=!0},pull:async t=>{if(e===0){t.close();return}let r;if(this.#t)r=this.#t,this.#t=void 0;else{let i=await this.#r.read();if(i.done){t.error(new Error("Unexpected end of stream"));return}r=i.value}let s=Math.min(e,r.length);t.enqueue(r.slice(0,s)),e-=s,e===0&&(this.#s(r.slice(s)),t.close())}})}get body(){let e=this.#e.fileSize,t=this.#n.getReader();return new ReadableStream({pull:async r=>{if(e===0){r.close();return}let s=await t.read();if(s.done){r.error(new Error("Unexpected end of stream"));return}let i=s.value,n=Math.min(e,i.length);if(r.enqueue(i.slice(0,n)),e-=n,e===0){for(;!(await t.read()).done;);r.close()}}})}async skip(){let e=this.#n.getReader();for(;!(await e.read()).done;);}async arrayBuffer(){let e=this.body.getReader(),t=new Uint8Array(this.#e.fileSize),r=0;for(;r<t.length;){let s=await e.read();if(s.done)throw new Error("Unexpected end of stream");t.set(s.value,r),r+=s.value.length}return t.buffer}async json(e="utf-8"){let t=await this.text(e);return JSON.parse(t)}async text(e="utf-8"){let t=await this.arrayBuffer();return new TextDecoder(e).decode(t)}};var u=new TextDecoder,h=class{#e;constructor(e){this.#e=e}get name(){let e=this.#e.slice(0,100),t=0;for(let r=0;r<e.length&&e[r]!==0;r++)t=r;return u.decode(e.slice(0,t+1))}get fileSize(){let e=this.#e.slice(124,136);return parseInt(u.decode(e),8)}get checksum(){let e=this.#e.slice(148,156);return parseInt(u.decode(e),8)}};var y=8*32;function g(a){let e=y;for(let t=0;t<512;t++)t>=148&&t<156||(e+=a[t]);return e}async function*v(a){let e=a.getReader(),t=new Uint8Array(512),r=0;for(;;){let{done:s,value:i}=await e.read();if(s)break;let n=i;for(;n.length>0;){let l=512-r,f=n.slice(0,l);if(n=n.slice(l),t.set(f,r),r+=f.length,r===512){let d=new h(t);if(g(t)!==d.checksum)return;let{resolve:w,promise:m}=new o;yield new c(d,e,n,w),d.fileSize>0&&(n=await m),r=0}}}}export{v as entries};

0 comments on commit b7badb3

Please sign in to comment.