From 5df865210048245f8d67d096a88a3f1f8e718408 Mon Sep 17 00:00:00 2001 From: Gorkem Ercan Date: Mon, 21 Oct 2024 12:36:54 -0400 Subject: [PATCH 1/6] Switch to llamafile for dev command harness Uses llamafile for harness functionality. Introduces downloading the harness in addition to embedding to allow for smaller binaries. Removes llama.cpp submodule Adds script for uploading llamafile to downloads. --- .gitignore | 3 + .gitmodules | 3 - pkg/lib/harness/generated/gen_common.sh | 99 ++++++++------- pkg/lib/harness/generated/gen_darwin.sh | 59 --------- .../{llmserver_darwin.go => llamafile.go} | 2 +- .../llamafile_ver_helper.go} | 18 +-- pkg/lib/harness/generated/llmserver_linux.go | 17 --- .../harness/generated/llmserver_windows.go | 17 --- .../harness/generated/refresh_downloads.sh | 87 +++++++++++++ pkg/lib/harness/llama.cpp | 1 - pkg/lib/harness/llm-harness.go | 76 ++++++++++-- pkg/lib/harness/llm_download.go | 117 ++++++++++++++++++ pkg/lib/harness/llm_payload.go | 43 ++++++- pkg/lib/harness/llm_payload_all.go | 39 +----- pkg/lib/harness/llm_payload_darwin_amd64.go | 25 ---- 15 files changed, 384 insertions(+), 222 deletions(-) mode change 100644 => 100755 pkg/lib/harness/generated/gen_common.sh delete mode 100644 pkg/lib/harness/generated/gen_darwin.sh rename pkg/lib/harness/generated/{llmserver_darwin.go => llamafile.go} (95%) rename pkg/lib/harness/{llm_payload_darwin_arm64.go => generated/llamafile_ver_helper.go} (75%) delete mode 100644 pkg/lib/harness/generated/llmserver_linux.go delete mode 100644 pkg/lib/harness/generated/llmserver_windows.go create mode 100755 pkg/lib/harness/generated/refresh_downloads.sh delete mode 160000 pkg/lib/harness/llama.cpp create mode 100644 pkg/lib/harness/llm_download.go delete mode 100644 pkg/lib/harness/llm_payload_darwin_amd64.go diff --git a/.gitignore b/.gitignore index a9886249..0ecc382c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ kit kit.exe dist/ pkg/lib/harness/ui.tar.gz +pkg/lib/harness/llamafile +pkg/lib/harness/checksums.txt +pkg/lib/harness/llamafile.tar.gz diff --git a/.gitmodules b/.gitmodules index a70386e6..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "pkg/lib/harness/llama.cpp"] - path = pkg/lib/harness/llama.cpp - url = https://github.com/ggerganov/llama.cpp.git diff --git a/pkg/lib/harness/generated/gen_common.sh b/pkg/lib/harness/generated/gen_common.sh old mode 100644 new mode 100755 index 17c17118..bc2102aa --- a/pkg/lib/harness/generated/gen_common.sh +++ b/pkg/lib/harness/generated/gen_common.sh @@ -1,50 +1,14 @@ -# common logic across linux and darwin +#!/bin/sh UI_DIR=../../../../frontend/dev-mode - -init_vars() { - case "${GOARCH}" in - "amd64") - ARCH="x86_64" - ;; - "arm64") - ARCH="arm64" - ;; - *) - ARCH=$(uname -m | sed -e "s/aarch64/arm64/g") - esac - - LLAMACPP_DIR=../llama.cpp - CMAKE_DEFS="" - CMAKE_TARGETS="--target server" - if echo "${CGO_CFLAGS}" | grep -- '-g' >/dev/null; then - CMAKE_DEFS="-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_VERBOSE_MAKEFILE=on -DLLAMA_GPROF=on -DLLAMA_SERVER_VERBOSE=on ${CMAKE_DEFS}" - else - CMAKE_DEFS="-DCMAKE_BUILD_TYPE=Release -DLLAMA_SERVER_VERBOSE=off ${CMAKE_DEFS}" - fi - if [ -z "${CMAKE_CUDA_ARCHITECTURES}" ] ; then - CMAKE_CUDA_ARCHITECTURES="50;52;61;70;75;80" - fi -} - -build() { - cmake -S ${LLAMACPP_DIR} -B ${BUILD_DIR} ${CMAKE_DEFS} - cmake --build ${BUILD_DIR} ${CMAKE_TARGETS} -j8 - # ls ${BUILD_DIR} -} - -git_module_setup() { - # Make sure the tree is clean - if [ -d "${LLAMACPP_DIR}/build/darwin/${GOARCH}/build" ]; then - echo "Cleaning up old submodule" - rm -rf ${LLAMACPP_DIR} - fi - git submodule init - git submodule update --force ${LLAMACPP_DIR} -} +LLAMAFILE_VER=$(go run ./llamafile_ver_helper.go) build_ui() { - pushd ${UI_DIR} + UI_HOME=$1 + + echo "Building harness UI from ${UI_HOME}" + + pushd ${UI_HOME} pnpm install pnpm run build popd @@ -53,4 +17,51 @@ build_ui() { compress() { echo "Compressing $1 to $2" tar -czf $2 -C $1 . -} \ No newline at end of file +} + +generate_sha() { + FILE=$1 + CHECKSUM_FILE=$2 + + echo "Generating SHA256 checksum for ${FILE}" + sha256sum ${FILE} | awk '{print $1 " " FILENAME}' FILENAME="${FILE}" >> ${CHECKSUM_FILE} + echo "Checksum for ${FILE} saved to ${CHECKSUM_FILE}" +} + + +# Function to download a binary asset from a GitHub release +download_github_release_binary() { + OWNER=$1 + REPO=$2 + RELEASE_TAG=$3 + ASSET_NAME=$4 + OUTPUT_DIR=$5 + + echo "Downloading asset '${ASSET_NAME}' from release '${RELEASE_TAG}' of repository '${OWNER}/${REPO}'" + + DOWNLOAD_URL="https://github.com/${OWNER}/${REPO}/releases/download/${RELEASE_TAG}/llamafile-${RELEASE_TAG}" + + mkdir -p ${OUTPUT_DIR} + + curl -L -o ${OUTPUT_DIR}/llamafile ${DOWNLOAD_URL} + + echo "Asset '${ASSET_NAME}' downloaded successfully to: ${OUTPUT_DIR}/${ASSET_NAME}" + + echo "${RELEASE_TAG}" > ${OUTPUT_DIR}/llamafile.version + + COMPRESSED_FILE="llamafile.tar.gz" + compress ${OUTPUT_DIR} ../${COMPRESSED_FILE} + + echo "Compressed asset saved to: ${COMPRESSED_FILE}" +} + +build_ui ${UI_DIR} +compress ${UI_DIR}/dist ../ui.tar.gz +download_github_release_binary "Mozilla-Ocho" "llamafile" "${LLAMAFILE_VER}" "llamafile-${LLAMAFILE_VER}" "./downloads" + +CHECKSUM_FILE="../checksums.txt" +> ${CHECKSUM_FILE} # Clear the checksum file if it exists +generate_sha "../ui.tar.gz" ${CHECKSUM_FILE} +generate_sha "../llamafile.tar.gz" ${CHECKSUM_FILE} + +echo "All checksums have been saved to ${CHECKSUM_FILE}" \ No newline at end of file diff --git a/pkg/lib/harness/generated/gen_darwin.sh b/pkg/lib/harness/generated/gen_darwin.sh deleted file mode 100644 index 55fe5547..00000000 --- a/pkg/lib/harness/generated/gen_darwin.sh +++ /dev/null @@ -1,59 +0,0 @@ -set -ex -set -o pipefail -echo "Starting Darwin LLM Harneess build" -source $(dirname $0)/gen_common.sh -init_vars -git_module_setup - -COMMON_DARWIN_DEFS="-DCMAKE_OSX_DEPLOYMENT_TARGET=11.3 -DLLAMA_METAL_MACOSX_VERSION_MIN=11.3 -DCMAKE_SYSTEM_NAME=Darwin -DLLAMA_METAL_EMBED_LIBRARY=on" - -## Build devmode UI -build_ui -compress ${UI_DIR}/dist ../ui.tar.gz - -case "${GOARCH}" in -"amd64") - COMMON_CPU_DEFS="${COMMON_DARWIN_DEFS} -DCMAKE_SYSTEM_PROCESSOR=${ARCH} -DCMAKE_OSX_ARCHITECTURES=${ARCH} -DLLAMA_METAL=off -DLLAMA_NATIVE=off" - - # - # CPU first for the default library, set up as lowest common denominator for maximum compatibility (including Rosetta) - # - - CMAKE_DEFS="${COMMON_CPU_DEFS} -DLLAMA_ACCELERATE=off -DLLAMA_AVX=off -DLLAMA_AVX2=off -DLLAMA_AVX512=off -DLLAMA_FMA=off -DLLAMA_F16C=off ${CMAKE_DEFS}" - BUILD_DIR="${LLAMACPP_DIR}/build/darwin/${ARCH}/cpu" - echo "Building LCD CPU" - build - - # - # ~2011 CPU Dynamic library with more capabilities turned on to optimize performance - # Approximately 400% faster than LCD on same CPU - # - init_vars - CMAKE_DEFS="${COMMON_CPU_DEFS} -DLLAMA_ACCELERATE=off -DLLAMA_AVX=on -DLLAMA_AVX2=off -DLLAMA_AVX512=off -DLLAMA_FMA=off -DLLAMA_F16C=off ${CMAKE_DEFS}" - BUILD_DIR="${LLAMACPP_DIR}/build/darwin/${ARCH}/cpu_avx" - echo "Building AVX CPU" - build - - # - # ~2013 CPU Dynamic library - # Approximately 10% faster than AVX on same CPU - # - init_vars - CMAKE_DEFS="${COMMON_CPU_DEFS} -DLLAMA_ACCELERATE=on -DLLAMA_AVX=on -DLLAMA_AVX2=on -DLLAMA_AVX512=off -DLLAMA_FMA=on -DLLAMA_F16C=on ${CMAKE_DEFS}" - BUILD_DIR="${LLAMACPP_DIR}/build/darwin/${ARCH}/cpu_avx2" - echo "Building AVX2 CPU" - EXTRA_LIBS="${EXTRA_LIBS} -framework Accelerate -framework Foundation" - build - ;; -"arm64") - CMAKE_DEFS="${COMMON_DARWIN_DEFS} -DLLAMA_METAL_EMBED_LIBRARY=on -DLLAMA_ACCELERATE=on -DCMAKE_SYSTEM_PROCESSOR=${ARCH} -DCMAKE_OSX_ARCHITECTURES=${ARCH} -DLLAMA_METAL=on ${CMAKE_DEFS}" - BUILD_DIR="${LLAMACPP_DIR}/build/darwin/${ARCH}/metal" - EXTRA_LIBS="${EXTRA_LIBS} -framework Accelerate -framework Foundation -framework Metal -framework MetalKit -framework MetalPerformanceShaders" - build - ;; -*) - echo "GOARCH must be set" - echo "this script is meant to be run from within go generate" - exit 1 - ;; -esac \ No newline at end of file diff --git a/pkg/lib/harness/generated/llmserver_darwin.go b/pkg/lib/harness/generated/llamafile.go similarity index 95% rename from pkg/lib/harness/generated/llmserver_darwin.go rename to pkg/lib/harness/generated/llamafile.go index 6c42a472..dc497930 100644 --- a/pkg/lib/harness/generated/llmserver_darwin.go +++ b/pkg/lib/harness/generated/llamafile.go @@ -15,4 +15,4 @@ // SPDX-License-Identifier: Apache-2.0 package generated -//go:generate sh ./gen_darwin.sh +//go:generate sh ./gen_common.sh diff --git a/pkg/lib/harness/llm_payload_darwin_arm64.go b/pkg/lib/harness/generated/llamafile_ver_helper.go similarity index 75% rename from pkg/lib/harness/llm_payload_darwin_arm64.go rename to pkg/lib/harness/generated/llamafile_ver_helper.go index b51cb524..1accbd4a 100644 --- a/pkg/lib/harness/llm_payload_darwin_arm64.go +++ b/pkg/lib/harness/generated/llamafile_ver_helper.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -14,12 +14,16 @@ // // SPDX-License-Identifier: Apache-2.0 -package harness +//go:build ignore +// +build ignore -import "embed" +package main -//go:embed llama.cpp/build/darwin/arm64/*/bin/* -var serverEmbed embed.FS +import ( + "fmt" + "kitops/pkg/lib/harness" +) -//go:embed ui.tar.gz -var uiEmbed embed.FS +func main() { + fmt.Println(harness.LlamaFileVersion) +} diff --git a/pkg/lib/harness/generated/llmserver_linux.go b/pkg/lib/harness/generated/llmserver_linux.go deleted file mode 100644 index c20c6d40..00000000 --- a/pkg/lib/harness/generated/llmserver_linux.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 The KitOps Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package generated diff --git a/pkg/lib/harness/generated/llmserver_windows.go b/pkg/lib/harness/generated/llmserver_windows.go deleted file mode 100644 index c20c6d40..00000000 --- a/pkg/lib/harness/generated/llmserver_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 The KitOps Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package generated diff --git a/pkg/lib/harness/generated/refresh_downloads.sh b/pkg/lib/harness/generated/refresh_downloads.sh new file mode 100755 index 00000000..78b30c9d --- /dev/null +++ b/pkg/lib/harness/generated/refresh_downloads.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -e + +# R2 Configuration from environment variables +R2_ACCESS_KEY_ID="${R2_ACCESS_KEY_ID}" +R2_SECRET_ACCESS_KEY="${R2_SECRET_ACCESS_KEY}" +R2_ENDPOINT="${R2_ENDPOINT}" + +R2_BUCKET_NAME="kitops-binaries" +REMOTE_NAME="r2" + +# Files to Upload +FILES_TO_UPLOAD=( + "llamafile.tar.gz" + "ui.tar.gz" + "checksums.txt" +) + + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +LLAMAFILE_VER=$(go run $SCRIPT_DIR/llamafile_ver_helper.go) + + + +# --------------------------- +# Functions +# --------------------------- + +configure_rclone() { + echo "Configuring rclone for Cloudflare R2..." + + # Create rclone configuration using environment variables + rclone config create "$REMOTE_NAME" s3 \ + provider Cloudflare \ + access_key_id "$R2_ACCESS_KEY_ID" \ + secret_access_key "$R2_SECRET_ACCESS_KEY" \ + endpoint "$R2_ENDPOINT" \ + region auto \ + acl private + + echo "rclone configured successfully." +} + +upload() { + SOURCE="$1" + DESTINATION="$2" + + FULL_DESTINATION="${REMOTE_NAME}:${R2_BUCKET_NAME}/${DESTINATION}" + + # Perform the upload using rclone copy + echo "Uploading '${SOURCE}' to '${FULL_DESTINATION}' ..." + rclone copy "$SOURCE" "$FULL_DESTINATION" --verbose + echo "Upload of '${SOURCE}' completed successfully." +} + +# --------------------------- +# Script Execution +# --------------------------- + +# Check if rclone is installed +if ! command -v rclone &> /dev/null +then + echo "Error: rclone could not be found. Please install rclone before running this script." + exit 1 +fi + +# Check if all required environment variables are set +if [[ -z "$R2_ACCESS_KEY_ID" || -z "$R2_SECRET_ACCESS_KEY" || -z "$R2_ENDPOINT" ]]; then + echo "Error: One or more required environment variables (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT) are not set." + exit 1 +fi + +configure_rclone + +for DEST_PATH in "${FILES_TO_UPLOAD[@]}"; do + FILE_PATH="${SCRIPT_DIR}/../${DEST_PATH}" + + if [ -f "$FILE_PATH" ]; then + echo "File found: '$FILE_PATH'. Preparing to upload..." + upload "$FILE_PATH" "$LLAMAFILE_VER/$DEST_PATH" + else + echo "Warning: File '$FILE_PATH' does not exist. Skipping upload." + fi +done + +echo "All files uploaded" diff --git a/pkg/lib/harness/llama.cpp b/pkg/lib/harness/llama.cpp deleted file mode 160000 index 433def28..00000000 --- a/pkg/lib/harness/llama.cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 433def286e98751bf17db75dce53847d075c0be5 diff --git a/pkg/lib/harness/llm-harness.go b/pkg/lib/harness/llm-harness.go index 2a2d58f7..22a46d42 100644 --- a/pkg/lib/harness/llm-harness.go +++ b/pkg/lib/harness/llm-harness.go @@ -22,13 +22,17 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" + "strings" "syscall" "kitops/pkg/lib/constants" "kitops/pkg/output" ) +const LlamaFileVersion = "0.8.14" + type LLMHarness struct { Host string Port int @@ -36,11 +40,19 @@ type LLMHarness struct { } func (harness *LLMHarness) Init() error { - err := extractServer(constants.HarnessPath(harness.ConfigHome), "llama.cpp/build/*/*/*/bin/**") + harnessPath := constants.HarnessPath(harness.ConfigHome) + ok, err := checkHarness(harnessPath) + if err != nil { + return fmt.Errorf("failed to verify dev server: %s", err) + } + if ok { + return nil + } + err = extractServer(harnessPath) if err != nil { return fmt.Errorf("failed to extract dev server files: %s", err) } - err = extractUI(constants.HarnessPath(harness.ConfigHome)) + err = extractUI(harnessPath) if err != nil { return fmt.Errorf("failed to extract dev UI files: %s", err) } @@ -67,11 +79,10 @@ func (harness *LLMHarness) Start(modelPath string) error { } uiHome := filepath.Join(harnessPath, "ui") - cmd := exec.Command("./server", - "--host", harness.Host, - "--port", strconv.Itoa(harness.Port), - "--model", modelPath, - "--path", uiHome) + cmd := exec.Command("sh", "-c", + fmt.Sprintf("./llamafile --server --model %s --host %s --port %d --path %s --nobrowser --unsecure", + modelPath, harness.Host, harness.Port, uiHome), + ) cmd.Dir = harnessPath logs, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { @@ -147,11 +158,11 @@ func PrintLogs(configHome string, w io.Writer) error { output.Errorf("No log file found") return nil } - return fmt.Errorf("Error reading log file: %w", err) + return fmt.Errorf("error reading log file: %w", err) } defer logFile.Close() if _, err = io.Copy(w, logFile); err != nil { - return fmt.Errorf("Failed to print log file: %w", err) + return fmt.Errorf("failed to print log file: %w", err) } return nil } @@ -195,3 +206,50 @@ func readPIDFromFile(filePath string) (int, error) { } return pid, nil } + +func checkHarness(harnessHome string) (bool, error) { + executableName := "llamafile" + if runtime.GOOS == "windows" { + executableName = "llamafile.exe" + } + llamaFilePath := filepath.Join(harnessHome, executableName) + llamaVersionPath := filepath.Join(harnessHome, "llamafile.version") + uiPath := filepath.Join(harnessHome, "ui") + + // 'llamafile' + if _, err := os.Stat(llamaFilePath); os.IsNotExist(err) { + return false, nil + } else if err != nil { + return false, fmt.Errorf("error checking 'llamafile': %w", err) + } + + // llamafile.version + if _, err := os.Stat(llamaVersionPath); os.IsNotExist(err) { + return false, nil + } else if err != nil { + return false, fmt.Errorf("error checking 'llamafile.version': %w", err) + } + + versionData, err := os.ReadFile(llamaVersionPath) + if err != nil { + return false, fmt.Errorf("error reading 'llamafile.version': %w", err) + } + version := strings.TrimSpace(string(versionData)) + if version != LlamaFileVersion { + return false, nil + } + + // 'ui/' + uiInfo, err := os.Stat(uiPath) + if os.IsNotExist(err) { + return false, nil + } else if err != nil { + return false, fmt.Errorf("error checking 'ui' directory: %w", err) + } + if !uiInfo.IsDir() { + return false, fmt.Errorf("'ui' exists but is not a directory") + } + + // harness is ready + return true, nil +} diff --git a/pkg/lib/harness/llm_download.go b/pkg/lib/harness/llm_download.go new file mode 100644 index 00000000..0a213c52 --- /dev/null +++ b/pkg/lib/harness/llm_download.go @@ -0,0 +1,117 @@ +// Copyright 2024 The KitOps Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build !embed_harness +// +build !embed_harness + +package harness + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" +) + +const ( + llamafileDownloadURL = "http://downloads.jozu.ml?file=llamafile.tar.gz&version=" + LlamaFileVersion + uiDownloadURL = "http://downloads.jozu.ml?file=ui.tar.gz&version=" + LlamaFileVersion +) + +func extractServer(harnessHome string) error { + if err := os.MkdirAll(harnessHome, 0o755); err != nil { + return fmt.Errorf("error creating directory %s: %w", harnessHome, err) + } + tmpFolder := filepath.Join(harnessHome, "tmp") + + err := downloadFile(llamafileDownloadURL, tmpFolder, "llamafile.tar.gz") + if err != nil { + return fmt.Errorf("failed to extract llamafile: %w", err) + } + localFS := os.DirFS(tmpFolder) + + err = extractFile(localFS, "llamafile.tar.gz", harnessHome) + if err != nil { + return fmt.Errorf("failed to unpack llamafile: %w", err) + } + + llamaFilePath := filepath.Join(harnessHome, "llamafile") + if runtime.GOOS == "windows" { + llamaExePath := filepath.Join(harnessHome, "llamafile.exe") + if err := os.Rename(llamaFilePath, llamaExePath); err != nil { + return fmt.Errorf("error renaming file to executable: %w", err) + } + } else { + if err := os.Chmod(llamaFilePath, 0o755); err != nil { + return fmt.Errorf("error setting executable permission: %w", err) + } + } + + return nil +} + +func extractUI(harnessHome string) error { + tmpFolder := filepath.Join(harnessHome, "tmp") + + err := downloadFile(uiDownloadURL, tmpFolder, "ui.tar.gz") + if err != nil { + return fmt.Errorf("failed to extract UI: %w", err) + } + + uiHome := filepath.Join(harnessHome, "ui") + if err := os.MkdirAll(uiHome, 0o755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", uiHome, err) + } + localFS := os.DirFS(tmpFolder) + + return extractFile(localFS, "ui.tar.gz", uiHome) +} + +func downloadFile(url string, folder string, filename string) error { + + err := os.MkdirAll(folder, 0o755) + if err != nil { + return fmt.Errorf("failed to create folder %s: %w", folder, err) + } + + filePath := filepath.Join(folder, filename) + + out, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", folder, err) + } + defer out.Close() + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("download from url %s failed: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status downloading file %s", resp.Status) + } + + _, err = io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("failed to write to file %s: %w", filePath, err) + } + + return nil + +} diff --git a/pkg/lib/harness/llm_payload.go b/pkg/lib/harness/llm_payload.go index bff19e08..a5c8f787 100644 --- a/pkg/lib/harness/llm_payload.go +++ b/pkg/lib/harness/llm_payload.go @@ -14,15 +14,54 @@ // // SPDX-License-Identifier: Apache-2.0 -//go:build !darwin -// +build !darwin +//go:build embed_harness +// +build embed_harness package harness import ( "embed" + "fmt" + "os" + "path/filepath" + "runtime" ) +//go:embed llamafile*.tar.gz var serverEmbed embed.FS +//go:embed ui.tar.gz var uiEmbed embed.FS + +func extractServer(harnessHome string) error { + // Create the harnessHome directory once before extracting files + if err := os.MkdirAll(harnessHome, 0o755); err != nil { + return fmt.Errorf("error creating directory %s: %w", harnessHome, err) + } + if err := extractFile(serverEmbed, "llamafile.tar.gz", harnessHome); err != nil { + return fmt.Errorf("error extracting file: %w", err) + } + + // Set executable permissions and rename on Windows + llamaFilePath := filepath.Join(harnessHome, "llamafile") + if runtime.GOOS == "windows" { + llamaExePath := filepath.Join(harnessHome, "llamafile.exe") + if err := os.Rename(llamaFilePath, llamaExePath); err != nil { + return fmt.Errorf("error renaming file to executable: %w", err) + } + } else { + if err := os.Chmod(llamaFilePath, 0o755); err != nil { + return fmt.Errorf("error setting executable permission: %w", err) + } + } + + return nil +} + +func extractUI(harnessHome string) error { + uiHome := filepath.Join(harnessHome, "ui") + if err := os.MkdirAll(uiHome, 0o755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", uiHome, err) + } + return extractFile(uiEmbed, "ui.tar.gz", uiHome) +} diff --git a/pkg/lib/harness/llm_payload_all.go b/pkg/lib/harness/llm_payload_all.go index f271654f..5371f6d8 100644 --- a/pkg/lib/harness/llm_payload_all.go +++ b/pkg/lib/harness/llm_payload_all.go @@ -19,7 +19,6 @@ package harness import ( "archive/tar" "compress/gzip" - "embed" "fmt" "io" "io/fs" @@ -28,43 +27,9 @@ import ( "strings" "kitops/pkg/lib/filesystem" - - "golang.org/x/sync/errgroup" ) -func extractServer(harnessHome string, glob string) error { - files, err := fs.Glob(serverEmbed, glob) - if err != nil { - return fmt.Errorf("error globbing files: %w", err) - } else if len(files) == 0 { - return fmt.Errorf("no files matched the glob pattern") - } - // Create the harnessHome directory once before extracting files - if err := os.MkdirAll(harnessHome, 0o755); err != nil { - return fmt.Errorf("error creating directory %s: %w", harnessHome, err) - } - - g := new(errgroup.Group) - for _, file := range files { - - file := file - g.Go(func() error { - return extractFile(serverEmbed, file, harnessHome) - }) - - } - return g.Wait() -} - -func extractUI(harnessHome string) error { - uiHome := filepath.Join(harnessHome, "ui") - if err := os.MkdirAll(uiHome, 0o755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", uiHome, err) - } - return extractFile(uiEmbed, "ui.tar.gz", uiHome) -} - -func extractFile(fs embed.FS, file, harnessHome string) error { +func extractFile(fs fs.FS, file, harnessHome string) error { srcFile, err := fs.Open(file) if err != nil { return fmt.Errorf("read payload %s: %v", file, err) @@ -147,7 +112,7 @@ func extractTar(tr *tar.Reader, dir string) error { } default: - return fmt.Errorf("Unrecognized type in archive: %s", header.Name) + return fmt.Errorf("unrecognized type in archive: %s", header.Name) } } return nil diff --git a/pkg/lib/harness/llm_payload_darwin_amd64.go b/pkg/lib/harness/llm_payload_darwin_amd64.go deleted file mode 100644 index 33d765ec..00000000 --- a/pkg/lib/harness/llm_payload_darwin_amd64.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2024 The KitOps Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package harness - -import "embed" - -//go:embed llama.cpp/build/darwin/x86_64/*/bin/* -var serverEmbed embed.FS - -//go:embed ui.tar.gz -var uiEmbed embed.FS From d4a0554a452d2db7f9bba6637f8150bb3bd81f3f Mon Sep 17 00:00:00 2001 From: Gorkem Ercan Date: Tue, 22 Oct 2024 19:06:29 -0400 Subject: [PATCH 2/6] Update build scripts Updates release scripts to include both embedded (offline) and download version. --- .github/workflows/platform-release.yaml | 2 ++ .gitignore | 1 + .goreleaser.darwin.yaml | 32 ++++++++++++++++++++++++- .goreleaser.linux.yaml | 25 +++++++++++++++++++ .goreleaser.windows.yaml | 25 +++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/.github/workflows/platform-release.yaml b/.github/workflows/platform-release.yaml index a4b7452b..8cb43a2f 100644 --- a/.github/workflows/platform-release.yaml +++ b/.github/workflows/platform-release.yaml @@ -79,7 +79,9 @@ jobs: shell: bash run: | ./build/scripts/sign ./dist/kitops-darwin-arm64.zip + ./build/scripts/sign ./dist/kitops-offline-darwin-arm64.zip ./build/scripts/sign ./dist/kitops-darwin-x86_64.zip + ./build/scripts/sign ./dist/kitops-offline-darwin-x86_64.zip - name: Upload macOS artifacts uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: diff --git a/.gitignore b/.gitignore index 0ecc382c..663c4692 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ kit kit.exe dist/ +pkg/lib/harness/generated/downloads/ pkg/lib/harness/ui.tar.gz pkg/lib/harness/llamafile pkg/lib/harness/checksums.txt diff --git a/.goreleaser.darwin.yaml b/.goreleaser.darwin.yaml index 8e5f0141..208db88c 100644 --- a/.goreleaser.darwin.yaml +++ b/.goreleaser.darwin.yaml @@ -5,7 +5,7 @@ project_name: kitops before: hooks: - go mod tidy - - ./build/scripts/generate-arch + - go generate ./... builds: - id: "kit-macos" @@ -23,6 +23,23 @@ builds: post: - cmd: ./build/scripts/sign '{{ .Path }}' output: true + - id: "kit-macos-offline" + flags: + - --tags=embed_harness + env: + - CGO_ENABLED=0 + goos: + - darwin + goarch: + - amd64 + - arm64 + binary: kit + ldflags: + - -s -w -X kitops/pkg/lib/constants.Version={{.Version}} -X kitops/pkg/lib/constants.GitCommit={{.Commit}} -X kitops/pkg/lib/constants.BuildTime={{.CommitDate}} + hooks: + post: + - cmd: ./build/scripts/sign '{{ .Path }}' + output: true archives: # zip archives for Mac OS: @@ -41,6 +58,19 @@ archives: files: - LICENSE - README.md + - id: kit-macos-zip-offline-archive + format: zip + builds: + - kit-macos-offline + name_template: >- + {{ .ProjectName }}-offline- + {{- tolower .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + files: + - LICENSE + - README.md # tar.gz archives for Mac OS: # - will not be notarized # - will be installable via homebrew (i.e. "brew install kitops") diff --git a/.goreleaser.linux.yaml b/.goreleaser.linux.yaml index 22118f20..8e2b5976 100644 --- a/.goreleaser.linux.yaml +++ b/.goreleaser.linux.yaml @@ -5,6 +5,7 @@ project_name: kitops before: hooks: - go mod tidy + - go generate ./... builds: - id: "kit-linux" @@ -15,6 +16,16 @@ builds: binary: kit ldflags: - -s -w -X kitops/pkg/lib/constants.Version={{.Version}} -X kitops/pkg/lib/constants.GitCommit={{.Commit}} -X kitops/pkg/lib/constants.BuildTime={{.CommitDate}} + - id: "kit-linux-embedded" + flags: + - --tags=embed_harness + env: + - CGO_ENABLED=0 + goos: + - linux + binary: kit + ldflags: + - -s -w -X kitops/pkg/lib/constants.Version={{.Version}} -X kitops/pkg/lib/constants.GitCommit={{.Commit}} -X kitops/pkg/lib/constants.BuildTime={{.CommitDate}} archives: - id: kit-archive @@ -31,6 +42,20 @@ archives: files: - LICENSE - README.md + - id: kit-archive-offile + format: tar.gz + builds: + - kit-linux-embedded + name_template: >- + {{ .ProjectName }}-offline- + {{- tolower .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + files: + - LICENSE + - README.md git: ignore_tags: diff --git a/.goreleaser.windows.yaml b/.goreleaser.windows.yaml index 6e798ee8..47f82677 100644 --- a/.goreleaser.windows.yaml +++ b/.goreleaser.windows.yaml @@ -5,6 +5,7 @@ project_name: kitops before: hooks: - go mod tidy + - go generate ./... builds: - id: "kit-wins" @@ -15,6 +16,16 @@ builds: binary: kit ldflags: - -s -w -X kitops/pkg/lib/constants.Version={{.Version}} -X kitops/pkg/lib/constants.GitCommit={{.Commit}} -X kitops/pkg/lib/constants.BuildTime={{.CommitDate}} + - id: "kit-wins-embedded" + flags: + - --tags=embed_harness + env: + - CGO_ENABLED=0 + goos: + - windows + binary: kit + ldflags: + - -s -w -X kitops/pkg/lib/constants.Version={{.Version}} -X kitops/pkg/lib/constants.GitCommit={{.Commit}} -X kitops/pkg/lib/constants.BuildTime={{.CommitDate}} archives: - id: kit-archive @@ -31,6 +42,20 @@ archives: files: - LICENSE - README.md + - id: kit-archive-offline + format: zip + builds: + - kit-wins + name_template: >- + {{ .ProjectName }}-offline- + {{- tolower .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + files: + - LICENSE + - README.md snapshot: name_template: "{{ .Version }}" \ No newline at end of file From 907c33789f3c9c84e56b4cdd533273d83723c169 Mon Sep 17 00:00:00 2001 From: Gorkem Ercan Date: Thu, 24 Oct 2024 10:29:14 -0400 Subject: [PATCH 3/6] Add checksum verify Adds checksum download and verify. Also minor improvements to error handling --- pkg/lib/harness/llm-harness.go | 18 ++-- pkg/lib/harness/llm_download.go | 129 ++++++++++++++++++++++++++--- pkg/lib/harness/llm_payload.go | 8 +- pkg/lib/harness/llm_payload_all.go | 37 +++++---- 4 files changed, 150 insertions(+), 42 deletions(-) diff --git a/pkg/lib/harness/llm-harness.go b/pkg/lib/harness/llm-harness.go index 22a46d42..76b35bf5 100644 --- a/pkg/lib/harness/llm-harness.go +++ b/pkg/lib/harness/llm-harness.go @@ -43,18 +43,18 @@ func (harness *LLMHarness) Init() error { harnessPath := constants.HarnessPath(harness.ConfigHome) ok, err := checkHarness(harnessPath) if err != nil { - return fmt.Errorf("failed to verify dev server: %s", err) + return fmt.Errorf("failed to verify dev server: %w", err) } if ok { return nil } err = extractServer(harnessPath) if err != nil { - return fmt.Errorf("failed to extract dev server files: %s", err) + return fmt.Errorf("failed to extract dev server files: %w", err) } err = extractUI(harnessPath) if err != nil { - return fmt.Errorf("failed to extract dev UI files: %s", err) + return fmt.Errorf("failed to extract dev UI files: %w", err) } return nil } @@ -94,7 +94,7 @@ func (harness *LLMHarness) Start(modelPath string) error { cmd.Stderr = logs if err := cmd.Start(); err != nil { - return fmt.Errorf("error starting llm harness: %s", err) + return fmt.Errorf("error starting llm harness: %w", err) } pid := cmd.Process.Pid @@ -126,16 +126,16 @@ func (harness *LLMHarness) Stop() error { // Kill the process using the PID. process, err := os.FindProcess(pid) if err != nil { - return fmt.Errorf("error finding process: %s", err) + return fmt.Errorf("error finding process: %w", err) } - err = process.Signal(syscall.SIGTERM) // Try to kill it gently + err = process.Signal(os.Interrupt) // Try to kill it gently if err != nil { - output.Infof("Error killing process %s", err) + output.Infof("Error killing process %w", err) // If SIGTERM failed, kill it with SIGKILL err = process.Kill() if err != nil { - return fmt.Errorf("error killing process: %s", err) + return fmt.Errorf("error killing process: %w", err) } } @@ -143,7 +143,7 @@ func (harness *LLMHarness) Stop() error { // Delete the PID file to clean up. err = os.Remove(pidFile) if err != nil { - return fmt.Errorf("error removing PID file: %s", err) + return fmt.Errorf("error removing PID file: %w", err) } return nil diff --git a/pkg/lib/harness/llm_download.go b/pkg/lib/harness/llm_download.go index 0a213c52..02ef98f7 100644 --- a/pkg/lib/harness/llm_download.go +++ b/pkg/lib/harness/llm_download.go @@ -20,29 +20,50 @@ package harness import ( + "bufio" + "crypto/sha256" + "encoding/hex" "fmt" "io" + "kitops/pkg/output" "net/http" "os" "path/filepath" "runtime" + "strings" ) const ( - llamafileDownloadURL = "http://downloads.jozu.ml?file=llamafile.tar.gz&version=" + LlamaFileVersion - uiDownloadURL = "http://downloads.jozu.ml?file=ui.tar.gz&version=" + LlamaFileVersion + llamafileDownloadURL = "https://jozu.ml/downloads/?file=llamafile.tar.gz&version=" + LlamaFileVersion + uiDownloadURL = "https://jozu.ml/downloads/?file=ui.tar.gz&version=" + LlamaFileVersion + checksumURL = "https://jozu.ml/downloads/?file=checksums.txt&version=" + LlamaFileVersion ) func extractServer(harnessHome string) error { - if err := os.MkdirAll(harnessHome, 0o755); err != nil { + if err := os.MkdirAll(harnessHome, os.FileMode(0755)); err != nil { return fmt.Errorf("error creating directory %s: %w", harnessHome, err) } - tmpFolder := filepath.Join(harnessHome, "tmp") + tmpFolder, err := os.MkdirTemp("", "kitops_tmp") + if err != nil { + return fmt.Errorf("failed to create temporary directory: %w", err) + } + defer os.RemoveAll(tmpFolder) - err := downloadFile(llamafileDownloadURL, tmpFolder, "llamafile.tar.gz") + output.Infoln("downloading harness binaries") + err = downloadFile(llamafileDownloadURL, tmpFolder, "llamafile.tar.gz") if err != nil { return fmt.Errorf("failed to extract llamafile: %w", err) } + // Download and verify checksum + checksums, err := downloadAndParseChecksums(tmpFolder) + if err != nil { + return fmt.Errorf("failed to download and parse checksums: %w", err) + } + err = verifyChecksum(tmpFolder, "llamafile.tar.gz", checksums) + if err != nil { + return fmt.Errorf("checksum verification failed for llamafile.tar.gz: %w", err) + } + localFS := os.DirFS(tmpFolder) err = extractFile(localFS, "llamafile.tar.gz", harnessHome) @@ -57,7 +78,7 @@ func extractServer(harnessHome string) error { return fmt.Errorf("error renaming file to executable: %w", err) } } else { - if err := os.Chmod(llamaFilePath, 0o755); err != nil { + if err := os.Chmod(llamaFilePath, os.FileMode(0755)); err != nil { return fmt.Errorf("error setting executable permission: %w", err) } } @@ -66,15 +87,31 @@ func extractServer(harnessHome string) error { } func extractUI(harnessHome string) error { - tmpFolder := filepath.Join(harnessHome, "tmp") + tmpFolder, err := os.MkdirTemp("", "kitops_tmp") + if err != nil { + return fmt.Errorf("failed to create temporary directory: %w", err) + } + defer os.RemoveAll(tmpFolder) - err := downloadFile(uiDownloadURL, tmpFolder, "ui.tar.gz") + output.Infoln("Updating harness UI components") + err = downloadFile(uiDownloadURL, tmpFolder, "ui.tar.gz") if err != nil { return fmt.Errorf("failed to extract UI: %w", err) } + // Download checksum.txt + checksums, err := downloadAndParseChecksums(tmpFolder) + if err != nil { + return fmt.Errorf("failed to download and parse checksums: %w", err) + } + // Verify checksum + err = verifyChecksum(tmpFolder, "ui.tar.gz", checksums) + if err != nil { + return fmt.Errorf("checksum verification failed for ui.tar.gz: %w", err) + } + uiHome := filepath.Join(harnessHome, "ui") - if err := os.MkdirAll(uiHome, 0o755); err != nil { + if err := os.MkdirAll(uiHome, os.FileMode(0755)); err != nil { return fmt.Errorf("failed to create directory %s: %w", uiHome, err) } localFS := os.DirFS(tmpFolder) @@ -84,7 +121,7 @@ func extractUI(harnessHome string) error { func downloadFile(url string, folder string, filename string) error { - err := os.MkdirAll(folder, 0o755) + err := os.MkdirAll(folder, os.FileMode(0755)) if err != nil { return fmt.Errorf("failed to create folder %s: %w", folder, err) } @@ -104,7 +141,8 @@ func downloadFile(url string, folder string, filename string) error { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status downloading file %s", resp.Status) + return fmt.Errorf("bad status downloading file from %s: %s", url, resp.Status) + } _, err = io.Copy(out, resp.Body) @@ -115,3 +153,72 @@ func downloadFile(url string, folder string, filename string) error { return nil } +func downloadAndParseChecksums(tmpFolder string) (map[string]string, error) { + err := downloadFile(checksumURL, tmpFolder, "checksums.txt") + if err != nil { + return nil, fmt.Errorf("failed to download checksums.txt: %w", err) + } + + checksumFilePath := filepath.Join(tmpFolder, "checksums.txt") + checksums, err := parseChecksumFile(checksumFilePath) + if err != nil { + return nil, fmt.Errorf("failed to parse checksums.txt: %w", err) + } + + return checksums, nil +} + +func verifyChecksum(folder, filename string, checksums map[string]string) error { + expectedChecksum, ok := checksums[filename] + if !ok { + return fmt.Errorf("checksum not found for file: %s", filename) + } + + filePath := filepath.Join(folder, filename) + fileData, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + hash := sha256.Sum256(fileData) + computedChecksum := hex.EncodeToString(hash[:]) + + if computedChecksum != expectedChecksum { + return fmt.Errorf("checksum mismatch for %s: expected %s, got %s", filename, expectedChecksum, computedChecksum) + } + + output.Infoln(fmt.Sprintf("Checksum verified for %s", filename)) + return nil +} + +func parseChecksumFile(filePath string) (map[string]string, error) { + checksums := make(map[string]string) + + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open checksum file: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // Skip empty lines + if strings.TrimSpace(line) == "" { + continue + } + parts := strings.Fields(line) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid checksum line: %s", line) + } + checksum := parts[0] + filename := filepath.Base(parts[1]) // Get the base name of the file (e.g., 'ui.tar.gz') + checksums[filename] = checksum + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading checksum file: %w", err) + } + + return checksums, nil +} diff --git a/pkg/lib/harness/llm_payload.go b/pkg/lib/harness/llm_payload.go index a5c8f787..05dea0f6 100644 --- a/pkg/lib/harness/llm_payload.go +++ b/pkg/lib/harness/llm_payload.go @@ -35,11 +35,11 @@ var uiEmbed embed.FS func extractServer(harnessHome string) error { // Create the harnessHome directory once before extracting files - if err := os.MkdirAll(harnessHome, 0o755); err != nil { + if err := os.MkdirAll(harnessHome, os.FileMode(0755)); err != nil { return fmt.Errorf("error creating directory %s: %w", harnessHome, err) } if err := extractFile(serverEmbed, "llamafile.tar.gz", harnessHome); err != nil { - return fmt.Errorf("error extracting file: %w", err) + return fmt.Errorf("error extracting file %s to %s: %w", "llamafile.tar.gz", harnessHome, err) } // Set executable permissions and rename on Windows @@ -50,7 +50,7 @@ func extractServer(harnessHome string) error { return fmt.Errorf("error renaming file to executable: %w", err) } } else { - if err := os.Chmod(llamaFilePath, 0o755); err != nil { + if err := os.Chmod(llamaFilePath, os.FileMode(0755)); err != nil { return fmt.Errorf("error setting executable permission: %w", err) } } @@ -60,7 +60,7 @@ func extractServer(harnessHome string) error { func extractUI(harnessHome string) error { uiHome := filepath.Join(harnessHome, "ui") - if err := os.MkdirAll(uiHome, 0o755); err != nil { + if err := os.MkdirAll(uiHome, os.FileMode(0755)); err != nil { return fmt.Errorf("failed to create directory %s: %w", uiHome, err) } return extractFile(uiEmbed, "ui.tar.gz", uiHome) diff --git a/pkg/lib/harness/llm_payload_all.go b/pkg/lib/harness/llm_payload_all.go index 5371f6d8..1fdde3eb 100644 --- a/pkg/lib/harness/llm_payload_all.go +++ b/pkg/lib/harness/llm_payload_all.go @@ -32,15 +32,17 @@ import ( func extractFile(fs fs.FS, file, harnessHome string) error { srcFile, err := fs.Open(file) if err != nil { - return fmt.Errorf("read payload %s: %v", file, err) + return fmt.Errorf("failed to open file %s: %w", file, err) } defer srcFile.Close() srcReader := io.Reader(srcFile) + destFileName := file + if strings.HasSuffix(file, ".tar.gz") { gzr, err := gzip.NewReader(srcReader) if err != nil { - return fmt.Errorf("error extracting gzipped file: %w", err) + return fmt.Errorf("failed to create gzip reader for %s: %w", file, err) } defer gzr.Close() tarReader := tar.NewReader(gzr) @@ -50,20 +52,20 @@ func extractFile(fs fs.FS, file, harnessHome string) error { if strings.HasSuffix(file, ".gz") { srcReader, err = gzip.NewReader(srcReader) if err != nil { - return fmt.Errorf("failed to decompress payload %s: %v", file, err) + return fmt.Errorf("failed to decompress payload %s: %w", file, err) } - file = strings.TrimSuffix(file, ".gz") + destFileName = strings.TrimSuffix(file, ".gz") } - destFile := filepath.Join(harnessHome, filepath.Base(file)) - dest, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) // Keep executable permissions + destFile := filepath.Join(harnessHome, filepath.Base(destFileName)) + dest, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0755)) // Keep executable permissions if err != nil { - return fmt.Errorf("write payload %s: %v", file, err) + return fmt.Errorf("failed to create destination file %s: %w", destFile, err) } defer dest.Close() if _, err := io.Copy(dest, srcReader); err != nil { - return fmt.Errorf("copy payload %s: %v", file, err) + return fmt.Errorf("failed to copy payload to %s: %w", destFile, err) } return nil } @@ -75,20 +77,19 @@ func extractTar(tr *tar.Reader, dir string) error { break } if err != nil { - return err + return fmt.Errorf("failed to read tar header: %w", err) + } + // Sanitize the file name to prevent path traversal + sanitizedName := filepath.Clean(header.Name) + if strings.Contains(sanitizedName, "..") || filepath.IsAbs(sanitizedName) { + return fmt.Errorf("invalid file path in archive: %s", sanitizedName) } - outPath := filepath.Join(dir, header.Name) + outPath := filepath.Join(dir, sanitizedName) switch header.Typeflag { case tar.TypeDir: - if fi, exists := filesystem.PathExists(outPath); exists { - if !fi.IsDir() { - return fmt.Errorf("path '%s' already exists and is not a directory", outPath) - } - } else { - if err := os.MkdirAll(outPath, header.FileInfo().Mode()); err != nil { - return fmt.Errorf("failed to create directory %s: %w", outPath, err) - } + if err := os.MkdirAll(outPath, header.FileInfo().Mode()); err != nil { + return fmt.Errorf("failed to create directory %s: %w", outPath, err) } case tar.TypeReg: From 0a30a70de3e77b77c4c734dc1e3865bc6be34e92 Mon Sep 17 00:00:00 2001 From: Gorkem Ercan Date: Fri, 25 Oct 2024 11:27:59 -0400 Subject: [PATCH 4/6] remove wins/linux restriction Removes the wins/linux restriction. Fixes the archive package for wins offline. --- pkg/cmd/dev/cmd.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/cmd/dev/cmd.go b/pkg/cmd/dev/cmd.go index 6c72e21b..19907911 100644 --- a/pkg/cmd/dev/cmd.go +++ b/pkg/cmd/dev/cmd.go @@ -18,7 +18,6 @@ package dev import ( "net" "os" - "runtime" "strconv" "kitops/pkg/lib/harness" @@ -82,12 +81,6 @@ func DevLogsCommand() *cobra.Command { func runStartCommand(opts *DevStartOptions) func(cmd *cobra.Command, args []string) { return func(cmd *cobra.Command, args []string) { - if runtime.GOOS == "windows" || runtime.GOOS == "linux" { - output.Infoln("Development server is not yet supported in this platform") - output.Infof("We are working to bring it to %s soon", runtime.GOOS) - return - } - if err := opts.complete(cmd.Context(), args); err != nil { output.Errorf("failed to complete options: %s", err) return From 3c59d729bb0662b7530e1245d620ace7f56b0bc8 Mon Sep 17 00:00:00 2001 From: Gorkem Ercan Date: Fri, 25 Oct 2024 16:42:12 -0400 Subject: [PATCH 5/6] windows fixes Fix the windows build archive and the exec command. --- .goreleaser.windows.yaml | 2 +- pkg/cmd/dev/dev.go | 2 +- pkg/lib/harness/llm-harness.go | 31 ++++++++++++++++++++++++++----- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.goreleaser.windows.yaml b/.goreleaser.windows.yaml index 47f82677..a6646b80 100644 --- a/.goreleaser.windows.yaml +++ b/.goreleaser.windows.yaml @@ -45,7 +45,7 @@ archives: - id: kit-archive-offline format: zip builds: - - kit-wins + - kit-wins-embedded name_template: >- {{ .ProjectName }}-offline- {{- tolower .Os }}- diff --git a/pkg/cmd/dev/dev.go b/pkg/cmd/dev/dev.go index ce6db863..1e4c5142 100644 --- a/pkg/cmd/dev/dev.go +++ b/pkg/cmd/dev/dev.go @@ -44,7 +44,7 @@ func runDev(ctx context.Context, options *DevStartOptions) error { if util.IsModelKitReference(kitfile.Model.Path) { resolvedKitfile, err := kfutils.ResolveKitfile(ctx, options.configHome, kitfile.Model.Path, kitfile.Model.Path) if err != nil { - return fmt.Errorf("Failed to resolve referenced modelkit %s: %w", kitfile.Model.Path, err) + return fmt.Errorf("failed to resolve referenced modelkit %s: %w", kitfile.Model.Path, err) } kitfile.Model.Path = resolvedKitfile.Model.Path kitfile.Model.Parts = append(kitfile.Model.Parts, resolvedKitfile.Model.Parts...) diff --git a/pkg/lib/harness/llm-harness.go b/pkg/lib/harness/llm-harness.go index 76b35bf5..bdb36f1a 100644 --- a/pkg/lib/harness/llm-harness.go +++ b/pkg/lib/harness/llm-harness.go @@ -60,6 +60,7 @@ func (harness *LLMHarness) Init() error { } func (harness *LLMHarness) Start(modelPath string) error { + harnessPath := constants.HarnessPath(harness.ConfigHome) pidFile := filepath.Join(harnessPath, constants.HarnessProcessFile) logFile := filepath.Join(harnessPath, constants.HarnessLogFile) @@ -79,10 +80,26 @@ func (harness *LLMHarness) Start(modelPath string) error { } uiHome := filepath.Join(harnessPath, "ui") - cmd := exec.Command("sh", "-c", - fmt.Sprintf("./llamafile --server --model %s --host %s --port %d --path %s --nobrowser --unsecure", - modelPath, harness.Host, harness.Port, uiHome), - ) + output.Debugf("model path is %s", modelPath) + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command( + "./llamafile.exe", + "--server", + "--model", modelPath, + "--host", harness.Host, + "--port", fmt.Sprintf("%d", harness.Port), + "--path", uiHome, + "--nobrowser", + "--unsecure", + ) + } else { + cmd = exec.Command("sh", "-c", + fmt.Sprintf("./llamafile --server --model %s --host %s --port %d --path %s --nobrowser --unsecure", + modelPath, harness.Host, harness.Port, uiHome), + ) + } + cmd.Dir = harnessPath logs, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { @@ -131,7 +148,7 @@ func (harness *LLMHarness) Stop() error { err = process.Signal(os.Interrupt) // Try to kill it gently if err != nil { - output.Infof("Error killing process %w", err) + output.Debugf("Error killing process %w", err) // If SIGTERM failed, kill it with SIGKILL err = process.Kill() if err != nil { @@ -172,6 +189,10 @@ func isProcessRunning(pid int) bool { if err != nil { return false } + if runtime.GOOS == "windows" { + // On Windows, just finding the process implies it exists + return true + } // Sending signal 0 to a process does not affect it but can be used for error checking. // If an error is returned, the process does not exist. err = process.Signal(syscall.Signal(0)) From e62218f0a880298b57ac7372b1b12b30c0d77434 Mon Sep 17 00:00:00 2001 From: Gorkem Ercan Date: Wed, 30 Oct 2024 12:57:30 -0400 Subject: [PATCH 6/6] minor updates Minor updates to address review comments --- .goreleaser.linux.yaml | 2 +- pkg/lib/harness/generated/llamafile_ver_helper.go | 2 +- pkg/lib/harness/llm-harness.go | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.goreleaser.linux.yaml b/.goreleaser.linux.yaml index 8e2b5976..ab895a39 100644 --- a/.goreleaser.linux.yaml +++ b/.goreleaser.linux.yaml @@ -42,7 +42,7 @@ archives: files: - LICENSE - README.md - - id: kit-archive-offile + - id: kit-archive-offline format: tar.gz builds: - kit-linux-embedded diff --git a/pkg/lib/harness/generated/llamafile_ver_helper.go b/pkg/lib/harness/generated/llamafile_ver_helper.go index 1accbd4a..0fd8237c 100644 --- a/pkg/lib/harness/generated/llamafile_ver_helper.go +++ b/pkg/lib/harness/generated/llamafile_ver_helper.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/lib/harness/llm-harness.go b/pkg/lib/harness/llm-harness.go index bdb36f1a..14ee21b1 100644 --- a/pkg/lib/harness/llm-harness.go +++ b/pkg/lib/harness/llm-harness.go @@ -17,8 +17,10 @@ package harness import ( + "errors" "fmt" "io" + "io/fs" "os" "os/exec" "path/filepath" @@ -238,14 +240,14 @@ func checkHarness(harnessHome string) (bool, error) { uiPath := filepath.Join(harnessHome, "ui") // 'llamafile' - if _, err := os.Stat(llamaFilePath); os.IsNotExist(err) { + if _, err := os.Stat(llamaFilePath); errors.Is(err, fs.ErrNotExist) { return false, nil } else if err != nil { return false, fmt.Errorf("error checking 'llamafile': %w", err) } // llamafile.version - if _, err := os.Stat(llamaVersionPath); os.IsNotExist(err) { + if _, err := os.Stat(llamaVersionPath); errors.Is(err, fs.ErrNotExist) { return false, nil } else if err != nil { return false, fmt.Errorf("error checking 'llamafile.version': %w", err) @@ -262,7 +264,7 @@ func checkHarness(harnessHome string) (bool, error) { // 'ui/' uiInfo, err := os.Stat(uiPath) - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return false, nil } else if err != nil { return false, fmt.Errorf("error checking 'ui' directory: %w", err)