diff --git a/gleam.toml b/gleam.toml index 0411da3..1bad0c2 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "esgleam" -version = "0.2.1" +version = "0.3.0" gleam = ">= 0.32.0" description = "esbuild for Gleam" @@ -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" diff --git a/manifest.toml b/manifest.toml index 79ff394..26e984e 100644 --- a/manifest.toml +++ b/manifest.toml @@ -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" } diff --git a/src/esgleam.gleam b/src/esgleam.gleam index b2b2612..903cfad 100644 --- a/src/esgleam.gleam +++ b/src/esgleam.gleam @@ -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=") diff --git a/src/esgleam/install.gleam b/src/esgleam/install.gleam index d76a200..5ec279e 100644 --- a/src/esgleam/install.gleam +++ b/src/esgleam/install.gleam @@ -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() } diff --git a/src/esgleam/internal/fetch_esbuild.gleam b/src/esgleam/internal/fetch_esbuild.gleam new file mode 100644 index 0000000..aae7fa9 --- /dev/null +++ b/src/esgleam/internal/fetch_esbuild.gleam @@ -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 +} diff --git a/src/esgleam/internal/platform.gleam b/src/esgleam/internal/platform.gleam new file mode 100644 index 0000000..7c607c5 --- /dev/null +++ b/src/esgleam/internal/platform.gleam @@ -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 +} diff --git a/src/ffi_esgleam.erl b/src/ffi_esgleam.erl index 8f5e796..da5c33f 100644 --- a/src/ffi_esgleam.erl +++ b/src/ffi_esgleam.erl @@ -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"]}]) +. diff --git a/src/ffi_esgleam.mjs b/src/ffi_esgleam.mjs index 6c7e7cb..7b62d3a 100644 --- a/src/ffi_esgleam.mjs +++ b/src/ffi_esgleam.mjs @@ -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 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 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; + } } } diff --git a/src/streaming_tar.mjs b/src/streaming_tar.mjs new file mode 100644 index 0000000..e760608 --- /dev/null +++ b/src/streaming_tar.mjs @@ -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=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};