diff --git a/.gitmodules b/.gitmodules index f025f0bd..01e8ca81 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,15 @@ [submodule "vendor/mimalloc"] path = vendor/mimalloc url = https://github.com/microsoft/mimalloc.git/ +[submodule "vendor/nghttp2"] + path = vendor/nghttp2 + url = https://github.com/nghttp2/nghttp2.git +[submodule "vendor/mbedtls"] + path = vendor/mbedtls + url = https://github.com/Mbed-TLS/mbedtls.git +[submodule "vendor/zlib"] + path = vendor/zlib + url = https://github.com/madler/zlib.git +[submodule "vendor/curl"] + path = vendor/curl + url = https://github.com/curl/curl.git diff --git a/build.zig b/build.zig index 6f3906f3..dc2e8bc4 100644 --- a/build.zig +++ b/build.zig @@ -19,11 +19,13 @@ const std = @import("std"); const builtin = @import("builtin"); +const Build = std.Build; + /// Do not rename this constant. It is scanned by some scripts to determine /// which zig version to install. const recommended_zig_version = "0.14.1"; -pub fn build(b: *std.Build) !void { +pub fn build(b: *Build) !void { switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) { .eq => {}, .lt => { @@ -138,29 +140,29 @@ pub fn build(b: *std.Build) !void { } } -fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void { +fn common(b: *Build, opts: *Build.Step.Options, step: *Build.Step.Compile) !void { const mod = step.root_module; const target = mod.resolved_target.?; const optimize = mod.optimize.?; const dep_opts = .{ .target = target, .optimize = optimize }; try moduleNetSurf(b, step, target); - mod.addImport("tls", b.dependency("tls", dep_opts).module("tls")); + mod.addImport("build_config", opts.createModule()); mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io")); + mod.addIncludePath(b.path("vendor/lightpanda")); + { // v8 + mod.link_libcpp = true; + const v8_opts = b.addOptions(); v8_opts.addOption(bool, "inspector_subtype", false); const v8_mod = b.dependency("v8", dep_opts).module("v8"); v8_mod.addOptions("default_exports", v8_opts); mod.addImport("v8", v8_mod); - } - mod.link_libcpp = true; - - { const release_dir = if (mod.optimize.? == .Debug) "debug" else "release"; const os = switch (target.result.os.tag) { .linux => "linux", @@ -181,21 +183,210 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co ); }; mod.addObjectFile(mod.owner.path(lib_path)); + + switch (target.result.os.tag) { + .macos => { + // v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation + mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" }); + mod.linkFramework("CoreFoundation", .{}); + }, + else => {}, + } } - switch (target.result.os.tag) { - .macos => { - // v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation - mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" }); - mod.linkFramework("CoreFoundation", .{}); - }, - else => {}, - } + { + //curl + { + const is_linux = target.result.os.tag == .linux; + if (is_linux) { + mod.addCMacro("HAVE_LINUX_TCP_H", "1"); + mod.addCMacro("HAVE_MSG_NOSIGNAL", "1"); + mod.addCMacro("HAVE_GETHOSTBYNAME_R", "1"); + } + mod.addCMacro("_FILE_OFFSET_BITS", "64"); + mod.addCMacro("BUILDING_LIBCURL", "1"); + mod.addCMacro("CURL_DISABLE_AWS", "1"); + mod.addCMacro("CURL_DISABLE_DICT", "1"); + mod.addCMacro("CURL_DISABLE_DOH", "1"); + mod.addCMacro("CURL_DISABLE_FILE", "1"); + mod.addCMacro("CURL_DISABLE_FTP", "1"); + mod.addCMacro("CURL_DISABLE_GOPHER", "1"); + mod.addCMacro("CURL_DISABLE_KERBEROS", "1"); + mod.addCMacro("CURL_DISABLE_IMAP", "1"); + mod.addCMacro("CURL_DISABLE_IPFS", "1"); + mod.addCMacro("CURL_DISABLE_LDAP", "1"); + mod.addCMacro("CURL_DISABLE_LDAPS", "1"); + mod.addCMacro("CURL_DISABLE_MQTT", "1"); + mod.addCMacro("CURL_DISABLE_NTLM", "1"); + mod.addCMacro("CURL_DISABLE_PROGRESS_METER", "1"); + mod.addCMacro("CURL_DISABLE_POP3", "1"); + mod.addCMacro("CURL_DISABLE_RTSP", "1"); + mod.addCMacro("CURL_DISABLE_SMB", "1"); + mod.addCMacro("CURL_DISABLE_SMTP", "1"); + mod.addCMacro("CURL_DISABLE_TELNET", "1"); + mod.addCMacro("CURL_DISABLE_TFTP", "1"); + mod.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__ ((__visibility__ (\"default\"))"); + mod.addCMacro("CURL_OS", if (is_linux) "\"Linux\"" else "\"mac\""); + mod.addCMacro("CURL_STATICLIB", "1"); + mod.addCMacro("ENABLE_IPV6", "1"); + mod.addCMacro("HAVE_ALARM", "1"); + mod.addCMacro("HAVE_ALLOCA_H", "1"); + mod.addCMacro("HAVE_ARPA_INET_H", "1"); + mod.addCMacro("HAVE_ARPA_TFTP_H", "1"); + mod.addCMacro("HAVE_ASSERT_H", "1"); + mod.addCMacro("HAVE_BASENAME", "1"); + mod.addCMacro("HAVE_BOOL_T", "1"); + mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1"); + mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1"); + mod.addCMacro("HAVE_DLFCN_H", "1"); + mod.addCMacro("HAVE_ERRNO_H", "1"); + mod.addCMacro("HAVE_FCNTL", "1"); + mod.addCMacro("HAVE_FCNTL_H", "1"); + mod.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1"); + mod.addCMacro("HAVE_FREEADDRINFO", "1"); + mod.addCMacro("HAVE_FSETXATTR", "1"); + mod.addCMacro("HAVE_FSETXATTR_5", "1"); + mod.addCMacro("HAVE_FTRUNCATE", "1"); + mod.addCMacro("HAVE_GETADDRINFO", "1"); + mod.addCMacro("HAVE_GETEUID", "1"); + mod.addCMacro("HAVE_GETHOSTBYNAME", "1"); + mod.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1"); + mod.addCMacro("HAVE_GETHOSTNAME", "1"); + mod.addCMacro("HAVE_GETPEERNAME", "1"); + mod.addCMacro("HAVE_GETPPID", "1"); + mod.addCMacro("HAVE_GETPPID", "1"); + mod.addCMacro("HAVE_GETPROTOBYNAME", "1"); + mod.addCMacro("HAVE_GETPWUID", "1"); + mod.addCMacro("HAVE_GETPWUID_R", "1"); + mod.addCMacro("HAVE_GETRLIMIT", "1"); + mod.addCMacro("HAVE_GETSOCKNAME", "1"); + mod.addCMacro("HAVE_GETTIMEOFDAY", "1"); + mod.addCMacro("HAVE_GMTIME_R", "1"); + mod.addCMacro("HAVE_IDN2_H", "1"); + mod.addCMacro("HAVE_IF_NAMETOINDEX", "1"); + mod.addCMacro("HAVE_IFADDRS_H", "1"); + mod.addCMacro("HAVE_INET_ADDR", "1"); + mod.addCMacro("HAVE_INET_PTON", "1"); + mod.addCMacro("HAVE_INTTYPES_H", "1"); + mod.addCMacro("HAVE_IOCTL", "1"); + mod.addCMacro("HAVE_IOCTL_FIONBIO", "1"); + mod.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1"); + mod.addCMacro("HAVE_LDAP_URL_PARSE", "1"); + mod.addCMacro("HAVE_LIBGEN_H", "1"); + mod.addCMacro("HAVE_LIBZ", "1"); + mod.addCMacro("HAVE_LL", "1"); + mod.addCMacro("HAVE_LOCALE_H", "1"); + mod.addCMacro("HAVE_LOCALTIME_R", "1"); + mod.addCMacro("HAVE_LONGLONG", "1"); + mod.addCMacro("HAVE_MALLOC_H", "1"); + mod.addCMacro("HAVE_MEMORY_H", "1"); + mod.addCMacro("HAVE_NET_IF_H", "1"); + mod.addCMacro("HAVE_NETDB_H", "1"); + mod.addCMacro("HAVE_NETINET_IN_H", "1"); + mod.addCMacro("HAVE_NETINET_TCP_H", "1"); + mod.addCMacro("HAVE_PIPE", "1"); + mod.addCMacro("HAVE_POLL", "1"); + mod.addCMacro("HAVE_POLL_FINE", "1"); + mod.addCMacro("HAVE_POLL_H", "1"); + mod.addCMacro("HAVE_POSIX_STRERROR_R", "1"); + mod.addCMacro("HAVE_PTHREAD_H", "1"); + mod.addCMacro("HAVE_PWD_H", "1"); + mod.addCMacro("HAVE_RECV", "1"); + mod.addCMacro("HAVE_SA_FAMILY_T", "1"); + mod.addCMacro("HAVE_SELECT", "1"); + mod.addCMacro("HAVE_SEND", "1"); + mod.addCMacro("HAVE_SETJMP_H", "1"); + mod.addCMacro("HAVE_SETLOCALE", "1"); + mod.addCMacro("HAVE_SETRLIMIT", "1"); + mod.addCMacro("HAVE_SETSOCKOPT", "1"); + mod.addCMacro("HAVE_SIGACTION", "1"); + mod.addCMacro("HAVE_SIGINTERRUPT", "1"); + mod.addCMacro("HAVE_SIGNAL", "1"); + mod.addCMacro("HAVE_SIGNAL_H", "1"); + mod.addCMacro("HAVE_SIGSETJMP", "1"); + mod.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1"); + mod.addCMacro("HAVE_SOCKET", "1"); + mod.addCMacro("HAVE_STDBOOL_H", "1"); + mod.addCMacro("HAVE_STDINT_H", "1"); + mod.addCMacro("HAVE_STDIO_H", "1"); + mod.addCMacro("HAVE_STDLIB_H", "1"); + mod.addCMacro("HAVE_STRCASECMP", "1"); + mod.addCMacro("HAVE_STRDUP", "1"); + mod.addCMacro("HAVE_STRERROR_R", "1"); + mod.addCMacro("HAVE_STRING_H", "1"); + mod.addCMacro("HAVE_STRINGS_H", "1"); + mod.addCMacro("HAVE_STRSTR", "1"); + mod.addCMacro("HAVE_STRTOK_R", "1"); + mod.addCMacro("HAVE_STRTOLL", "1"); + mod.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1"); + mod.addCMacro("HAVE_STRUCT_TIMEVAL", "1"); + mod.addCMacro("HAVE_SYS_IOCTL_H", "1"); + mod.addCMacro("HAVE_SYS_PARAM_H", "1"); + mod.addCMacro("HAVE_SYS_POLL_H", "1"); + mod.addCMacro("HAVE_SYS_RESOURCE_H", "1"); + mod.addCMacro("HAVE_SYS_SELECT_H", "1"); + mod.addCMacro("HAVE_SYS_SOCKET_H", "1"); + mod.addCMacro("HAVE_SYS_STAT_H", "1"); + mod.addCMacro("HAVE_SYS_TIME_H", "1"); + mod.addCMacro("HAVE_SYS_TYPES_H", "1"); + mod.addCMacro("HAVE_SYS_UIO_H", "1"); + mod.addCMacro("HAVE_SYS_UN_H", "1"); + mod.addCMacro("HAVE_TERMIO_H", "1"); + mod.addCMacro("HAVE_TERMIOS_H", "1"); + mod.addCMacro("HAVE_TIME_H", "1"); + mod.addCMacro("HAVE_UNAME", "1"); + mod.addCMacro("HAVE_UNISTD_H", "1"); + mod.addCMacro("HAVE_UTIME", "1"); + mod.addCMacro("HAVE_UTIME_H", "1"); + mod.addCMacro("HAVE_UTIMES", "1"); + mod.addCMacro("HAVE_VARIADIC_MACROS_C99", "1"); + mod.addCMacro("HAVE_VARIADIC_MACROS_GCC", "1"); + mod.addCMacro("HAVE_ZLIB_H", "1"); + mod.addCMacro("RANDOM_FILE", "\"/dev/urandom\""); + mod.addCMacro("RECV_TYPE_ARG1", "int"); + mod.addCMacro("RECV_TYPE_ARG2", "void *"); + mod.addCMacro("RECV_TYPE_ARG3", "size_t"); + mod.addCMacro("RECV_TYPE_ARG4", "int"); + mod.addCMacro("RECV_TYPE_RETV", "ssize_t"); + mod.addCMacro("SEND_QUAL_ARG2", "const"); + mod.addCMacro("SEND_TYPE_ARG1", "int"); + mod.addCMacro("SEND_TYPE_ARG2", "void *"); + mod.addCMacro("SEND_TYPE_ARG3", "size_t"); + mod.addCMacro("SEND_TYPE_ARG4", "int"); + mod.addCMacro("SEND_TYPE_RETV", "ssize_t"); + mod.addCMacro("SIZEOF_CURL_OFF_T", "8"); + mod.addCMacro("SIZEOF_INT", "4"); + mod.addCMacro("SIZEOF_LONG", "8"); + mod.addCMacro("SIZEOF_OFF_T", "8"); + mod.addCMacro("SIZEOF_SHORT", "2"); + mod.addCMacro("SIZEOF_SIZE_T", "8"); + mod.addCMacro("SIZEOF_TIME_T", "8"); + mod.addCMacro("STDC_HEADERS", "1"); + mod.addCMacro("TIME_WITH_SYS_TIME", "1"); + mod.addCMacro("USE_NGHTTP2", "1"); + mod.addCMacro("USE_MBEDTLS", "1"); + mod.addCMacro("USE_THREADS_POSIX", "1"); + mod.addCMacro("USE_UNIX_SOCKETS", "1"); + } - mod.addImport("build_config", opts.createModule()); + try buildZlib(b, mod); + try buildMbedtls(b, mod); + try buildNghttp2(b, mod); + try buildCurl(b, mod); + + switch (target.result.os.tag) { + .macos => { + // needed for proxying on mac + mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" }); + mod.linkFramework("CoreFoundation", .{}); + mod.linkFramework("SystemConfiguration", .{}); + }, + else => {}, + } + } } -fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void { +fn moduleNetSurf(b: *Build, step: *Build.Step.Compile, target: std.Build.ResolvedTarget) !void { const os = target.result.os.tag; const arch = target.result.cpu.arch; @@ -250,3 +441,375 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src")); } } + +fn buildZlib(b: *Build, m: *Build.Module) !void { + const zlib = b.addLibrary(.{ + .name = "zlib", + .root_module = m, + }); + + const root = "vendor/zlib/"; + zlib.installHeader(b.path(root ++ "zlib.h"), "zlib.h"); + zlib.installHeader(b.path(root ++ "zconf.h"), "zconf.h"); + zlib.addCSourceFiles(.{ .flags = &.{ + "-DHAVE_SYS_TYPES_H", + "-DHAVE_STDINT_H", + "-DHAVE_STDDEF_H", + }, .files = &.{ + root ++ "adler32.c", + root ++ "compress.c", + root ++ "crc32.c", + root ++ "deflate.c", + root ++ "gzclose.c", + root ++ "gzlib.c", + root ++ "gzread.c", + root ++ "gzwrite.c", + root ++ "inflate.c", + root ++ "infback.c", + root ++ "inftrees.c", + root ++ "inffast.c", + root ++ "trees.c", + root ++ "uncompr.c", + root ++ "zutil.c", + } }); +} + +fn buildMbedtls(b: *Build, m: *Build.Module) !void { + const mbedtls = b.addLibrary(.{ + .name = "mbedtls", + .root_module = m, + }); + + const root = "vendor/mbedtls/"; + mbedtls.addIncludePath(b.path(root ++ "include")); + mbedtls.addIncludePath(b.path(root ++ "library")); + + mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{ + root ++ "library/aes.c", + root ++ "library/aesni.c", + root ++ "library/aesce.c", + root ++ "library/aria.c", + root ++ "library/asn1parse.c", + root ++ "library/asn1write.c", + root ++ "library/base64.c", + root ++ "library/bignum.c", + root ++ "library/bignum_core.c", + root ++ "library/bignum_mod.c", + root ++ "library/bignum_mod_raw.c", + root ++ "library/camellia.c", + root ++ "library/ccm.c", + root ++ "library/chacha20.c", + root ++ "library/chachapoly.c", + root ++ "library/cipher.c", + root ++ "library/cipher_wrap.c", + root ++ "library/constant_time.c", + root ++ "library/cmac.c", + root ++ "library/ctr_drbg.c", + root ++ "library/des.c", + root ++ "library/dhm.c", + root ++ "library/ecdh.c", + root ++ "library/ecdsa.c", + root ++ "library/ecjpake.c", + root ++ "library/ecp.c", + root ++ "library/ecp_curves.c", + root ++ "library/entropy.c", + root ++ "library/entropy_poll.c", + root ++ "library/error.c", + root ++ "library/gcm.c", + root ++ "library/hkdf.c", + root ++ "library/hmac_drbg.c", + root ++ "library/lmots.c", + root ++ "library/lms.c", + root ++ "library/md.c", + root ++ "library/md5.c", + root ++ "library/memory_buffer_alloc.c", + root ++ "library/nist_kw.c", + root ++ "library/oid.c", + root ++ "library/padlock.c", + root ++ "library/pem.c", + root ++ "library/pk.c", + root ++ "library/pk_ecc.c", + root ++ "library/pk_wrap.c", + root ++ "library/pkcs12.c", + root ++ "library/pkcs5.c", + root ++ "library/pkparse.c", + root ++ "library/pkwrite.c", + root ++ "library/platform.c", + root ++ "library/platform_util.c", + root ++ "library/poly1305.c", + root ++ "library/psa_crypto.c", + root ++ "library/psa_crypto_aead.c", + root ++ "library/psa_crypto_cipher.c", + root ++ "library/psa_crypto_client.c", + root ++ "library/psa_crypto_ffdh.c", + root ++ "library/psa_crypto_driver_wrappers_no_static.c", + root ++ "library/psa_crypto_ecp.c", + root ++ "library/psa_crypto_hash.c", + root ++ "library/psa_crypto_mac.c", + root ++ "library/psa_crypto_pake.c", + root ++ "library/psa_crypto_rsa.c", + root ++ "library/psa_crypto_se.c", + root ++ "library/psa_crypto_slot_management.c", + root ++ "library/psa_crypto_storage.c", + root ++ "library/psa_its_file.c", + root ++ "library/psa_util.c", + root ++ "library/ripemd160.c", + root ++ "library/rsa.c", + root ++ "library/rsa_alt_helpers.c", + root ++ "library/sha1.c", + root ++ "library/sha3.c", + root ++ "library/sha256.c", + root ++ "library/sha512.c", + root ++ "library/threading.c", + root ++ "library/timing.c", + root ++ "library/version.c", + root ++ "library/version_features.c", + root ++ "library/pkcs7.c", + root ++ "library/x509.c", + root ++ "library/x509_create.c", + root ++ "library/x509_crl.c", + root ++ "library/x509_crt.c", + root ++ "library/x509_csr.c", + root ++ "library/x509write.c", + root ++ "library/x509write_crt.c", + root ++ "library/x509write_csr.c", + root ++ "library/debug.c", + root ++ "library/mps_reader.c", + root ++ "library/mps_trace.c", + root ++ "library/net_sockets.c", + root ++ "library/ssl_cache.c", + root ++ "library/ssl_ciphersuites.c", + root ++ "library/ssl_client.c", + root ++ "library/ssl_cookie.c", + root ++ "library/ssl_debug_helpers_generated.c", + root ++ "library/ssl_msg.c", + root ++ "library/ssl_ticket.c", + root ++ "library/ssl_tls.c", + root ++ "library/ssl_tls12_client.c", + root ++ "library/ssl_tls12_server.c", + root ++ "library/ssl_tls13_keys.c", + root ++ "library/ssl_tls13_server.c", + root ++ "library/ssl_tls13_client.c", + root ++ "library/ssl_tls13_generic.c", + } }); +} + +fn buildNghttp2(b: *Build, m: *Build.Module) !void { + const nghttp2 = b.addLibrary(.{ + .name = "nghttp2", + .root_module = m, + }); + + const root = "vendor/nghttp2/"; + nghttp2.addIncludePath(b.path(root ++ "lib")); + nghttp2.addIncludePath(b.path(root ++ "lib/includes")); + nghttp2.addCSourceFiles(.{ .flags = &.{ + "-DNGHTTP2_STATICLIB", + "-DHAVE_NETINET_IN", + "-DHAVE_TIME_H", + }, .files = &.{ + root ++ "lib/sfparse.c", + root ++ "lib/nghttp2_alpn.c", + root ++ "lib/nghttp2_buf.c", + root ++ "lib/nghttp2_callbacks.c", + root ++ "lib/nghttp2_debug.c", + root ++ "lib/nghttp2_extpri.c", + root ++ "lib/nghttp2_frame.c", + root ++ "lib/nghttp2_hd.c", + root ++ "lib/nghttp2_hd_huffman.c", + root ++ "lib/nghttp2_hd_huffman_data.c", + root ++ "lib/nghttp2_helper.c", + root ++ "lib/nghttp2_http.c", + root ++ "lib/nghttp2_map.c", + root ++ "lib/nghttp2_mem.c", + root ++ "lib/nghttp2_option.c", + root ++ "lib/nghttp2_outbound_item.c", + root ++ "lib/nghttp2_pq.c", + root ++ "lib/nghttp2_priority_spec.c", + root ++ "lib/nghttp2_queue.c", + root ++ "lib/nghttp2_rcbuf.c", + root ++ "lib/nghttp2_session.c", + root ++ "lib/nghttp2_stream.c", + root ++ "lib/nghttp2_submit.c", + root ++ "lib/nghttp2_version.c", + root ++ "lib/nghttp2_ratelim.c", + root ++ "lib/nghttp2_time.c", + } }); +} + +fn buildCurl(b: *Build, m: *Build.Module) !void { + const curl = b.addLibrary(.{ + .name = "curl", + .root_module = m, + }); + + const root = "vendor/curl/"; + + curl.addIncludePath(b.path(root ++ "lib")); + curl.addIncludePath(b.path(root ++ "include")); + curl.addCSourceFiles(.{ + .flags = &.{}, + .files = &.{ + root ++ "lib/altsvc.c", + root ++ "lib/amigaos.c", + root ++ "lib/asyn-ares.c", + root ++ "lib/asyn-base.c", + root ++ "lib/asyn-thrdd.c", + root ++ "lib/bufq.c", + root ++ "lib/bufref.c", + root ++ "lib/cf-h1-proxy.c", + root ++ "lib/cf-h2-proxy.c", + root ++ "lib/cf-haproxy.c", + root ++ "lib/cf-https-connect.c", + root ++ "lib/cf-socket.c", + root ++ "lib/cfilters.c", + root ++ "lib/conncache.c", + root ++ "lib/connect.c", + root ++ "lib/content_encoding.c", + root ++ "lib/cookie.c", + root ++ "lib/cshutdn.c", + root ++ "lib/curl_addrinfo.c", + root ++ "lib/curl_des.c", + root ++ "lib/curl_endian.c", + root ++ "lib/curl_fnmatch.c", + root ++ "lib/curl_get_line.c", + root ++ "lib/curl_gethostname.c", + root ++ "lib/curl_gssapi.c", + root ++ "lib/curl_memrchr.c", + root ++ "lib/curl_ntlm_core.c", + root ++ "lib/curl_range.c", + root ++ "lib/curl_rtmp.c", + root ++ "lib/curl_sasl.c", + root ++ "lib/curl_sha512_256.c", + root ++ "lib/curl_sspi.c", + root ++ "lib/curl_threads.c", + root ++ "lib/curl_trc.c", + root ++ "lib/cw-out.c", + root ++ "lib/cw-pause.c", + root ++ "lib/dict.c", + root ++ "lib/doh.c", + root ++ "lib/dynhds.c", + root ++ "lib/easy.c", + root ++ "lib/easygetopt.c", + root ++ "lib/easyoptions.c", + root ++ "lib/escape.c", + root ++ "lib/fake_addrinfo.c", + root ++ "lib/file.c", + root ++ "lib/fileinfo.c", + root ++ "lib/fopen.c", + root ++ "lib/formdata.c", + root ++ "lib/ftp.c", + root ++ "lib/ftplistparser.c", + root ++ "lib/getenv.c", + root ++ "lib/getinfo.c", + root ++ "lib/gopher.c", + root ++ "lib/hash.c", + root ++ "lib/headers.c", + root ++ "lib/hmac.c", + root ++ "lib/hostip.c", + root ++ "lib/hostip4.c", + root ++ "lib/hostip6.c", + root ++ "lib/hsts.c", + root ++ "lib/http.c", + root ++ "lib/http1.c", + root ++ "lib/http2.c", + root ++ "lib/http_aws_sigv4.c", + root ++ "lib/http_chunks.c", + root ++ "lib/http_digest.c", + root ++ "lib/http_negotiate.c", + root ++ "lib/http_ntlm.c", + root ++ "lib/http_proxy.c", + root ++ "lib/httpsrr.c", + root ++ "lib/idn.c", + root ++ "lib/if2ip.c", + root ++ "lib/imap.c", + root ++ "lib/krb5.c", + root ++ "lib/ldap.c", + root ++ "lib/llist.c", + root ++ "lib/macos.c", + root ++ "lib/md4.c", + root ++ "lib/md5.c", + root ++ "lib/memdebug.c", + root ++ "lib/mime.c", + root ++ "lib/mprintf.c", + root ++ "lib/mqtt.c", + root ++ "lib/multi.c", + root ++ "lib/multi_ev.c", + root ++ "lib/netrc.c", + root ++ "lib/noproxy.c", + root ++ "lib/openldap.c", + root ++ "lib/parsedate.c", + root ++ "lib/pingpong.c", + root ++ "lib/pop3.c", + root ++ "lib/progress.c", + root ++ "lib/psl.c", + root ++ "lib/rand.c", + root ++ "lib/rename.c", + root ++ "lib/request.c", + root ++ "lib/rtsp.c", + root ++ "lib/select.c", + root ++ "lib/sendf.c", + root ++ "lib/setopt.c", + root ++ "lib/sha256.c", + root ++ "lib/share.c", + root ++ "lib/slist.c", + root ++ "lib/smb.c", + root ++ "lib/smtp.c", + root ++ "lib/socketpair.c", + root ++ "lib/socks.c", + root ++ "lib/socks_gssapi.c", + root ++ "lib/socks_sspi.c", + root ++ "lib/speedcheck.c", + root ++ "lib/splay.c", + root ++ "lib/strcase.c", + root ++ "lib/strdup.c", + root ++ "lib/strequal.c", + root ++ "lib/strerror.c", + root ++ "lib/system_win32.c", + root ++ "lib/telnet.c", + root ++ "lib/tftp.c", + root ++ "lib/transfer.c", + root ++ "lib/uint-bset.c", + root ++ "lib/uint-hash.c", + root ++ "lib/uint-spbset.c", + root ++ "lib/uint-table.c", + root ++ "lib/url.c", + root ++ "lib/urlapi.c", + root ++ "lib/version.c", + root ++ "lib/ws.c", + root ++ "lib/curlx/base64.c", + root ++ "lib/curlx/dynbuf.c", + root ++ "lib/curlx/inet_ntop.c", + root ++ "lib/curlx/nonblock.c", + root ++ "lib/curlx/strparse.c", + root ++ "lib/curlx/timediff.c", + root ++ "lib/curlx/timeval.c", + root ++ "lib/curlx/wait.c", + root ++ "lib/curlx/warnless.c", + root ++ "lib/vquic/curl_ngtcp2.c", + root ++ "lib/vquic/curl_osslq.c", + root ++ "lib/vquic/curl_quiche.c", + root ++ "lib/vquic/vquic.c", + root ++ "lib/vquic/vquic-tls.c", + root ++ "lib/vauth/cleartext.c", + root ++ "lib/vauth/cram.c", + root ++ "lib/vauth/digest.c", + root ++ "lib/vauth/digest_sspi.c", + root ++ "lib/vauth/gsasl.c", + root ++ "lib/vauth/krb5_gssapi.c", + root ++ "lib/vauth/krb5_sspi.c", + root ++ "lib/vauth/ntlm.c", + root ++ "lib/vauth/ntlm_sspi.c", + root ++ "lib/vauth/oauth2.c", + root ++ "lib/vauth/spnego_gssapi.c", + root ++ "lib/vauth/spnego_sspi.c", + root ++ "lib/vauth/vauth.c", + root ++ "lib/vtls/cipher_suite.c", + root ++ "lib/vtls/mbedtls.c", + root ++ "lib/vtls/mbedtls_threadlock.c", + root ++ "lib/vtls/vtls.c", + root ++ "lib/vtls/vtls_scache.c", + root ++ "lib/vtls/x509asn1.c", + }, + }); +} diff --git a/build.zig.zon b/build.zig.zon index b51e82da..6a45c2d8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,10 +4,6 @@ .version = "0.0.0", .fingerprint = 0xda130f3af836cea0, .dependencies = .{ - .tls = .{ - .url = "https://github.com/ianic/tls.zig/archive/55845f755d9e2e821458ea55693f85c737cd0c7a.tar.gz", - .hash = "tls-0.1.0-ER2e0m43BQAshi8ixj1qf3w2u2lqKtXtkrxUJ4AGZDcl", - }, .tigerbeetle_io = .{ .url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz", .hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd", diff --git a/flake.lock b/flake.lock index f593473f..1c140996 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748964450, - "narHash": "sha256-ZouDiXkUk8mkMnah10QcoQ9Nu6UW6AFAHLScS3En6aI=", + "lastModified": 1754919767, + "narHash": "sha256-bc9tjR2ymbmbtYlnOcksjI7tQtDDEEJFGm41t0msXsg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9ff500cd9e123f46c55855eca64beccead29b152", + "rev": "8c0c41355297485b39d6f6a6d722c8cdfe0257df", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 95efc02e..d902ef89 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,7 @@ glib.dev glibc.dev zlib + zlib.dev ]; }; in diff --git a/src/app.zig b/src/app.zig index ad66f73a..a1d7ee30 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,9 +1,10 @@ const std = @import("std"); + const Allocator = std.mem.Allocator; const log = @import("log.zig"); +const Http = @import("http/Http.zig"); const Loop = @import("runtime/loop.zig").Loop; -const http = @import("http/client.zig"); const Platform = @import("runtime/js.zig").Platform; const Telemetry = @import("telemetry/telemetry.zig").Telemetry; @@ -12,12 +13,12 @@ const Notification = @import("notification.zig").Notification; // Container for global state / objects that various parts of the system // might need. pub const App = struct { + http: Http, loop: *Loop, config: Config, platform: ?*const Platform, allocator: Allocator, telemetry: Telemetry, - http_client: http.Client, app_dir_path: ?[]const u8, notification: *Notification, @@ -32,9 +33,12 @@ pub const App = struct { run_mode: RunMode, platform: ?*const Platform = null, tls_verify_host: bool = true, - http_proxy: ?std.Uri = null, - proxy_type: ?http.ProxyType = null, - proxy_auth: ?http.ProxyAuth = null, + http_proxy: ?[:0]const u8 = null, + proxy_bearer_token: ?[:0]const u8 = null, + http_timeout_ms: ?u31 = null, + http_connect_timeout_ms: ?u31 = null, + http_max_host_open: ?u8 = null, + http_max_concurrent: ?u8 = null, }; pub fn init(allocator: Allocator, config: Config) !*App { @@ -50,25 +54,33 @@ pub const App = struct { const notification = try Notification.init(allocator, null); errdefer notification.deinit(); + var http = try Http.init(allocator, .{ + .max_host_open = config.http_max_host_open orelse 4, + .max_concurrent = config.http_max_concurrent orelse 10, + .timeout_ms = config.http_timeout_ms orelse 5000, + .connect_timeout_ms = config.http_connect_timeout_ms orelse 0, + .http_proxy = config.http_proxy, + .tls_verify_host = config.tls_verify_host, + .proxy_bearer_token = config.proxy_bearer_token, + }); + errdefer http.deinit(); + const app_dir_path = getAndMakeAppDir(allocator); app.* = .{ .loop = loop, + .http = http, .allocator = allocator, .telemetry = undefined, .platform = config.platform, .app_dir_path = app_dir_path, .notification = notification, - .http_client = try http.Client.init(allocator, loop, .{ - .max_concurrent = 3, - .http_proxy = config.http_proxy, - .proxy_type = config.proxy_type, - .proxy_auth = config.proxy_auth, - .tls_verify_host = config.tls_verify_host, - }), .config = config, }; - app.telemetry = Telemetry.init(app, config.run_mode); + + app.telemetry = try Telemetry.init(app, config.run_mode); + errdefer app.telemetry.deinit(); + try app.telemetry.register(app.notification); return app; @@ -82,8 +94,8 @@ pub const App = struct { self.telemetry.deinit(); self.loop.deinit(); allocator.destroy(self.loop); - self.http_client.deinit(); self.notification.deinit(); + self.http.deinit(); allocator.destroy(self); } }; diff --git a/src/browser/DataURI.zig b/src/browser/DataURI.zig new file mode 100644 index 00000000..00d3792f --- /dev/null +++ b/src/browser/DataURI.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +// Parses data:[][;base64], +pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 { + if (!std.mem.startsWith(u8, src, "data:")) { + return null; + } + + const uri = src[5..]; + const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null; + + var data = uri[data_starts + 1 ..]; + + // Extract the encoding. + const metadata = uri[0..data_starts]; + if (std.mem.endsWith(u8, metadata, ";base64")) { + const decoder = std.base64.standard.Decoder; + const decoded_size = try decoder.calcSizeForSlice(data); + + const buffer = try allocator.alloc(u8, decoded_size); + errdefer allocator.free(buffer); + + try decoder.decode(buffer, data); + data = buffer; + } + + return data; +} + +const testing = @import("../testing.zig"); +test "DataURI: parse valid" { + try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo"); + try test_valid("data:text/javascript; charset=utf-8;,foo", "foo"); + try test_valid("data:,foo", "foo"); +} + +test "DataURI: parse invalid" { + try test_cannot_parse("atad:,foo"); + try test_cannot_parse("data:foo"); + try test_cannot_parse("data:"); +} + +fn test_valid(uri: []const u8, expected: []const u8) !void { + defer testing.reset(); + const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed; + try testing.expectEqual(expected, data_uri); +} + +fn test_cannot_parse(uri: []const u8) !void { + try testing.expectEqual(null, parse(undefined, uri)); +} diff --git a/src/browser/Scheduler.zig b/src/browser/Scheduler.zig new file mode 100644 index 00000000..fbdb8105 --- /dev/null +++ b/src/browser/Scheduler.zig @@ -0,0 +1,168 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const log = @import("../log.zig"); +const Allocator = std.mem.Allocator; + +const Scheduler = @This(); + +primary: Queue, + +// For repeating tasks. We only want to run these if there are other things to +// do. We don't, for example, want a window.setInterval or the page.runMicrotasks +// to block the page.wait. +secondary: Queue, + +// we expect allocator to be the page arena, hence we never call primary.deinit +pub fn init(allocator: Allocator) Scheduler { + return .{ + .primary = Queue.init(allocator, {}), + .secondary = Queue.init(allocator, {}), + }; +} + +pub fn reset(self: *Scheduler) void { + self.primary.clearRetainingCapacity(); + self.secondary.clearRetainingCapacity(); +} + +const AddOpts = struct { + name: []const u8 = "", +}; +pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void { + if (ms > 5_000) { + log.warn(.user_script, "long timeout ignored", .{ .delay = ms }); + // ignore any task that we're almost certainly never going to run + return; + } + return self.primary.add(.{ + .ms = std.time.milliTimestamp() + ms, + .ctx = ctx, + .func = func, + .name = opts.name, + }); +} + +pub fn runHighPriority(self: *Scheduler) !?u32 { + return self.runQueue(&self.primary); +} + +pub fn runLowPriority(self: *Scheduler) !?u32 { + return self.runQueue(&self.secondary); +} + +fn runQueue(self: *Scheduler, queue: *Queue) !?u32 { + // this is O(1) + if (queue.count() == 0) { + return null; + } + + const now = std.time.milliTimestamp(); + + var next = queue.peek(); + while (next) |task| { + const time_to_next = task.ms - now; + if (time_to_next > 0) { + // @intCast is petty safe since we limit tasks to just 5 seconds + // in the future + return @intCast(time_to_next); + } + + if (task.func(task.ctx)) |repeat_delay| { + // if we do (now + 0) then our WHILE loop will run endlessly. + // no task should ever return 0 + std.debug.assert(repeat_delay != 0); + + var copy = task; + copy.ms = now + repeat_delay; + try self.secondary.add(copy); + } + _ = queue.remove(); + next = queue.peek(); + } + return null; +} + +const Task = struct { + ms: i64, + func: Func, + ctx: *anyopaque, + name: []const u8, + + const Func = *const fn (ctx: *anyopaque) ?u32; +}; + +const Queue = std.PriorityQueue(Task, void, struct { + fn compare(_: void, a: Task, b: Task) std.math.Order { + return std.math.order(a.ms, b.ms); + } +}.compare); + +const testing = @import("../testing.zig"); +test "Scheduler" { + defer testing.reset(); + + var task = TestTask{ .allocator = testing.arena_allocator }; + + var s = Scheduler.init(testing.arena_allocator); + try testing.expectEqual(null, s.runHighPriority()); + try testing.expectEqual(0, task.calls.items.len); + + try s.add(&task, TestTask.run1, 3, .{}); + + try testing.expectDelta(3, try s.runHighPriority(), 1); + try testing.expectEqual(0, task.calls.items.len); + + std.time.sleep(std.time.ns_per_ms * 5); + try testing.expectEqual(null, s.runHighPriority()); + try testing.expectEqualSlices(u32, &.{1}, task.calls.items); + + try s.add(&task, TestTask.run2, 3, .{}); + try s.add(&task, TestTask.run1, 2, .{}); + + std.time.sleep(std.time.ns_per_ms * 5); + try testing.expectDelta(null, try s.runHighPriority(), 1); + try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items); + + std.time.sleep(std.time.ns_per_ms * 5); + // wont' run secondary + try testing.expectEqual(null, try s.runHighPriority()); + try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items); + + //runs secondary + try testing.expectDelta(2, try s.runLowPriority(), 1); + try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items); +} + +const TestTask = struct { + allocator: Allocator, + calls: std.ArrayListUnmanaged(u32) = .{}, + + fn run1(ctx: *anyopaque) ?u32 { + var self: *TestTask = @alignCast(@ptrCast(ctx)); + self.calls.append(self.allocator, 1) catch unreachable; + return null; + } + + fn run2(ctx: *anyopaque) ?u32 { + var self: *TestTask = @alignCast(@ptrCast(ctx)); + self.calls.append(self.allocator, 2) catch unreachable; + return 2; + } +}; diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig new file mode 100644 index 00000000..3d5b86bd --- /dev/null +++ b/src/browser/ScriptManager.zig @@ -0,0 +1,835 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const log = @import("../log.zig"); +const parser = @import("netsurf.zig"); + +const Env = @import("env.zig").Env; +const Page = @import("page.zig").Page; +const DataURI = @import("DataURI.zig"); +const Browser = @import("browser.zig").Browser; +const HttpClient = @import("../http/Client.zig"); +const URL = @import("../url.zig").URL; + +const Allocator = std.mem.Allocator; +const ArrayListUnmanaged = std.ArrayListUnmanaged; + +const ScriptManager = @This(); + +page: *Page, + +// used to prevent recursive evalution +is_evaluating: bool, + +// used to prevent executing scripts while we're doing a blocking load +is_blocking: bool = false, + +// Only once this is true can deferred scripts be run +static_scripts_done: bool, + +// List of async scripts. We don't care about the execution order of these, but +// on shutdown/abort, we need to co cleanup any pending ones. +asyncs: OrderList, + +// Normal scripts (non-deffered & non-async). These must be executed ni order +scripts: OrderList, + +// List of deferred scripts. These must be executed in order, but only once +// dom_loaded == true, +deferreds: OrderList, + +shutdown: bool = false, + +client: *HttpClient, +allocator: Allocator, +buffer_pool: BufferPool, +script_pool: std.heap.MemoryPool(PendingScript), + +const OrderList = std.DoublyLinkedList(*PendingScript); + +pub fn init(browser: *Browser, page: *Page) ScriptManager { + // page isn't fully initialized, we can setup our reference, but that's it. + const allocator = browser.allocator; + return .{ + .page = page, + .asyncs = .{}, + .scripts = .{}, + .deferreds = .{}, + .is_evaluating = false, + .allocator = allocator, + .client = browser.http_client, + .static_scripts_done = false, + .buffer_pool = BufferPool.init(allocator, 5), + .script_pool = std.heap.MemoryPool(PendingScript).init(allocator), + }; +} + +pub fn deinit(self: *ScriptManager) void { + self.reset(); + self.buffer_pool.deinit(); + self.script_pool.deinit(); +} + +pub fn reset(self: *ScriptManager) void { + self.clearList(&self.asyncs); + self.clearList(&self.scripts); + self.clearList(&self.deferreds); + self.static_scripts_done = false; +} + +fn clearList(_: *const ScriptManager, list: *OrderList) void { + while (list.first) |node| { + const pending_script = node.data; + // this removes it from the list + pending_script.deinit(); + } + std.debug.assert(list.first == null); +} + +pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void { + if (try parser.elementGetAttribute(element, "nomodule") != null) { + // these scripts should only be loaded if we don't support modules + // but since we do support modules, we can just skip them. + return; + } + + // If a script tag gets dynamically created and added to the dom: + // document.getElementsByTagName('head')[0].appendChild(script) + // that script tag will immediately get executed by our scriptAddedCallback. + // However, if the location where the script tag is inserted happens to be + // below where processHTMLDoc curently is, then we'll re-run that same script + // again in processHTMLDoc. This flag is used to let us know if a specific + // +// Unlike external modules which can only ever be executed after releasing an +// http handle, these are executed without there necessarily being a free handle. +// Thus, Http/Client.zig maintains a dedicated handle for these calls. +pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult { + std.debug.assert(self.is_blocking == false); + + self.is_blocking = true; + defer { + self.is_blocking = false; + + // we blocked evaluation while loading this script, there could be + // scripts ready to process. + self.evaluate(); + } + + var blocking = Blocking{ + .allocator = self.allocator, + .buffer_pool = &self.buffer_pool, + }; + + var headers = try HttpClient.Headers.init(); + try self.page.requestCookie(.{}).headersForRequest(self.allocator, url, &headers); + + var client = self.client; + try client.blockingRequest(.{ + .url = url, + .method = .GET, + .headers = headers, + .cookie_jar = self.page.cookie_jar, + .ctx = &blocking, + .start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null, + .header_done_callback = Blocking.headerCallback, + .data_callback = Blocking.dataCallback, + .done_callback = Blocking.doneCallback, + .error_callback = Blocking.errorCallback, + }); + + // rely on http's timeout settings to avoid an endless/long loop. + while (true) { + try client.tick(200); + switch (blocking.state) { + .running => {}, + .done => |result| return result, + .err => |err| return err, + } + } +} + +pub fn staticScriptsDone(self: *ScriptManager) void { + std.debug.assert(self.static_scripts_done == false); + self.static_scripts_done = true; +} + +// try to evaluate completed scripts (in order). This is called whenever a script +// is completed. +fn evaluate(self: *ScriptManager) void { + if (self.is_evaluating) { + // It's possible for a script.eval to cause evaluate to be called again. + // This is particularly true with blockingGet, but even without this, + // it's theoretically possible (but unlikely). We could make this work + // but there's little reason to support the complexity. + return; + } + + if (self.is_blocking) { + // Cannot evaluate scripts while a blocking-load is in progress. Not + // only could that result in incorrect evaluation order, it could + // triger another blocking request, while we're doing a blocking request. + return; + } + + const page = self.page; + self.is_evaluating = true; + defer self.is_evaluating = false; + + while (self.scripts.first) |n| { + var pending_script = n.data; + if (pending_script.complete == false) { + return; + } + defer pending_script.deinit(); + pending_script.script.eval(page); + } + + if (self.static_scripts_done == false) { + // We can only execute deferred scripts if + // 1 - all the normal scripts are done + // 2 - we've finished parsing the HTML and at least queued all the scripts + // The last one isn't obvious, but it's possible for self.scripts to + // be empty not because we're done executing all the normal scripts + // but because we're done executing some (or maybe none), but we're still + // parsing the HTML. + return; + } + + while (self.deferreds.first) |n| { + var pending_script = n.data; + if (pending_script.complete == false) { + return; + } + defer pending_script.deinit(); + pending_script.script.eval(page); + } + + // When all scripts (normal and deferred) are done loading, the document + // state changes (this ultimately triggers the DOMContentLoaded event) + page.documentIsLoaded(); + + if (self.asyncs.first == null) { + // if we're here, then its like `asyncDone` + // 1 - there are no async scripts pending + // 2 - we checkecked static_scripts_done == true above + // 3 - we drained self.scripts above + // 4 - we drained self.deferred above + page.documentIsComplete(); + } +} + +pub fn isDone(self: *const ScriptManager) bool { + return self.asyncs.first == null and // there are no more async scripts + self.static_scripts_done and // and we've finished parsing the HTML to queue all + self.scripts.first == null and // and there are no more