Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
201a987ff9 | ||
|
2d8125042a | ||
|
776e7bb5e4 | ||
|
b8d7ca1a7b |
1
app/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
ollama.syso
|
ollama.syso
|
||||||
|
app
|
7
app/AppDelegate.h
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||||
|
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||||
|
|
||||||
|
@end
|
@ -1,10 +1,6 @@
|
|||||||
# Ollama App
|
# Ollama App
|
||||||
|
|
||||||
## Linux
|
## macOS
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## MacOS
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
|
76
app/app_darwin.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// #cgo CFLAGS: -x objective-c
|
||||||
|
// #cgo LDFLAGS: -framework Cocoa -framework LocalAuthentication -framework ServiceManagement
|
||||||
|
// #include "app_darwin.h"
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerLogFile = filepath.Join(home, ".ollama", "logs", "server.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
initLogging()
|
||||||
|
slog.Info("ollama macOS app started")
|
||||||
|
|
||||||
|
// Ask to move to applications directory
|
||||||
|
moving := C.askToMoveToApplications()
|
||||||
|
if moving {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
C.killOtherInstances()
|
||||||
|
|
||||||
|
code := C.installSymlink()
|
||||||
|
if code != 0 {
|
||||||
|
slog.Error("Failed to install symlink")
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var options ServerOptions
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
var done chan int
|
||||||
|
|
||||||
|
done, err = SpawnServer(ctx, filepath.Join(filepath.Dir(exe), "..", "Resources", "ollama"), options)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err))
|
||||||
|
done = make(chan int, 1)
|
||||||
|
done <- 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the native macOS app
|
||||||
|
// Note: this will block until the app is closed
|
||||||
|
C.run()
|
||||||
|
|
||||||
|
slog.Info("ollama macOS app closed")
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
slog.Info("Waiting for ollama server to shutdown...")
|
||||||
|
if done != nil {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
slog.Info("Ollama app exiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
//export Quit
|
||||||
|
func Quit() {
|
||||||
|
syscall.Kill(os.Getpid(), syscall.SIGTERM)
|
||||||
|
}
|
13
app/app_darwin.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||||
|
@end
|
||||||
|
|
||||||
|
void run();
|
||||||
|
void killOtherInstances();
|
||||||
|
bool askToMoveToApplications();
|
||||||
|
int createSymlinkWithAuthorization();
|
||||||
|
int installSymlink();
|
||||||
|
extern void Restart();
|
||||||
|
extern void Quit();
|
282
app/app_darwin.m
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <CoreServices/CoreServices.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
#import <ServiceManagement/ServiceManagement.h>
|
||||||
|
#import "app_darwin.h"
|
||||||
|
|
||||||
|
@interface AppDelegate ()
|
||||||
|
|
||||||
|
@property (strong, nonatomic) NSStatusItem *statusItem;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||||
|
// show status menu
|
||||||
|
NSMenu *menu = [[NSMenu alloc] init];
|
||||||
|
|
||||||
|
NSMenuItem *aboutMenuItem = [[NSMenuItem alloc] initWithTitle:@"About Ollama" action:@selector(aboutOllama) keyEquivalent:@""];
|
||||||
|
[aboutMenuItem setTarget:self];
|
||||||
|
[menu addItem:aboutMenuItem];
|
||||||
|
|
||||||
|
// Settings submenu
|
||||||
|
NSMenu *settingsMenu = [[NSMenu alloc] initWithTitle:@"Settings"];
|
||||||
|
|
||||||
|
// Submenu items
|
||||||
|
NSMenuItem *chooseModelDirectoryItem = [[NSMenuItem alloc] initWithTitle:@"Choose model directory..." action:@selector(chooseModelDirectory) keyEquivalent:@""];
|
||||||
|
[chooseModelDirectoryItem setTarget:self];
|
||||||
|
[chooseModelDirectoryItem setEnabled:YES];
|
||||||
|
[settingsMenu addItem:chooseModelDirectoryItem];
|
||||||
|
|
||||||
|
NSMenuItem *exposeExternallyItem = [[NSMenuItem alloc] initWithTitle:@"Allow external connections" action:@selector(toggleExposeExternally:) keyEquivalent:@""];
|
||||||
|
[exposeExternallyItem setTarget:self];
|
||||||
|
[exposeExternallyItem setState:NSOffState]; // Set initial state to off
|
||||||
|
[exposeExternallyItem setEnabled:YES];
|
||||||
|
[settingsMenu addItem:exposeExternallyItem];
|
||||||
|
|
||||||
|
NSMenuItem *allowCrossOriginItem = [[NSMenuItem alloc] initWithTitle:@"Allow browser requests" action:@selector(toggleCrossOrigin:) keyEquivalent:@""];
|
||||||
|
[allowCrossOriginItem setTarget:self];
|
||||||
|
[allowCrossOriginItem setState:NSOffState]; // Set initial state to off
|
||||||
|
[allowCrossOriginItem setEnabled:YES];
|
||||||
|
[settingsMenu addItem:allowCrossOriginItem];
|
||||||
|
|
||||||
|
NSMenuItem *settingsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Settings" action:nil keyEquivalent:@""];
|
||||||
|
[settingsMenuItem setSubmenu:settingsMenu];
|
||||||
|
[menu addItem:settingsMenuItem];
|
||||||
|
|
||||||
|
[menu addItemWithTitle:@"Quit Ollama" action:@selector(quit) keyEquivalent:@"q"];
|
||||||
|
|
||||||
|
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
|
||||||
|
[self.statusItem addObserver:self forKeyPath:@"button.effectiveAppearance" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil];
|
||||||
|
|
||||||
|
self.statusItem.menu = menu;
|
||||||
|
[self showIcon];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)aboutOllama {
|
||||||
|
[[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)toggleCrossOrigin:(id)sender {
|
||||||
|
NSMenuItem *item = (NSMenuItem *)sender;
|
||||||
|
if ([item state] == NSOffState) {
|
||||||
|
// Do something when cross-origin requests are allowed
|
||||||
|
[item setState:NSOnState];
|
||||||
|
} else {
|
||||||
|
// Do something when cross-origin requests are disallowed
|
||||||
|
[item setState:NSOffState];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)toggleExposeExternally:(id)sender {
|
||||||
|
NSMenuItem *item = (NSMenuItem *)sender;
|
||||||
|
if ([item state] == NSOffState) {
|
||||||
|
// Do something when Ollama is exposed externally
|
||||||
|
[item setState:NSOnState];
|
||||||
|
} else {
|
||||||
|
// Do something when Ollama is not exposed externally
|
||||||
|
[item setState:NSOffState];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)chooseModelDirectory {
|
||||||
|
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
|
||||||
|
[openPanel setCanChooseFiles:NO];
|
||||||
|
[openPanel setCanChooseDirectories:YES];
|
||||||
|
[openPanel setAllowsMultipleSelection:NO];
|
||||||
|
|
||||||
|
NSInteger result = [openPanel runModal];
|
||||||
|
if (result == NSModalResponseOK) {
|
||||||
|
NSURL *selectedDirectoryURL = [openPanel URLs].firstObject;
|
||||||
|
// Do something with the selected directory URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void) showIcon {
|
||||||
|
NSAppearance* appearance = self.statusItem.button.effectiveAppearance;
|
||||||
|
NSString* appearanceName = (NSString*)(appearance.name);
|
||||||
|
NSString* iconName = [[appearanceName lowercaseString] containsString:@"dark"] ? @"iconDark" : @"icon";
|
||||||
|
NSImage* statusImage = [NSImage imageNamed:iconName];
|
||||||
|
[statusImage setTemplate:YES];
|
||||||
|
self.statusItem.button.image = statusImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
|
||||||
|
[self showIcon];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)quit {
|
||||||
|
[NSApp stop:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
@autoreleasepool {
|
||||||
|
[NSApplication sharedApplication];
|
||||||
|
AppDelegate *appDelegate = [[AppDelegate alloc] init];
|
||||||
|
[NSApp setDelegate:appDelegate];
|
||||||
|
[NSApp run];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// killOtherInstances kills all other instances of the app currently
|
||||||
|
// running. This way we can ensure that only the most recently started
|
||||||
|
// instance of Ollama is running
|
||||||
|
void killOtherInstances() {
|
||||||
|
pid_t pid = getpid();
|
||||||
|
NSArray *all = [[NSWorkspace sharedWorkspace] runningApplications];
|
||||||
|
NSMutableArray *apps = [NSMutableArray array];
|
||||||
|
|
||||||
|
for (NSRunningApplication *app in all) {
|
||||||
|
if ([app.bundleIdentifier isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] ||
|
||||||
|
[app.bundleIdentifier isEqualToString:@"ai.ollama.ollama"] ||
|
||||||
|
[app.bundleIdentifier isEqualToString:@"com.electron.ollama"]) {
|
||||||
|
if (app.processIdentifier != pid) {
|
||||||
|
[apps addObject:app];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSRunningApplication *app in apps) {
|
||||||
|
kill(app.processIdentifier, SIGTERM);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSDate *startTime = [NSDate date];
|
||||||
|
for (NSRunningApplication *app in apps) {
|
||||||
|
while (!app.terminated) {
|
||||||
|
if (-[startTime timeIntervalSinceNow] >= 5) {
|
||||||
|
kill(app.processIdentifier, SIGKILL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool askToMoveToApplications() {
|
||||||
|
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
||||||
|
if ([bundlePath hasPrefix:@"/Applications"]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSAlert *alert = [[NSAlert alloc] init];
|
||||||
|
[alert setMessageText:@"Move to Applications?"];
|
||||||
|
[alert setInformativeText:@"Ollama works best when run from the Applications directory."];
|
||||||
|
[alert addButtonWithTitle:@"Move to Applications"];
|
||||||
|
[alert addButtonWithTitle:@"Don't move"];
|
||||||
|
|
||||||
|
[NSApp activateIgnoringOtherApps:YES];
|
||||||
|
|
||||||
|
if ([alert runModal] != NSAlertFirstButtonReturn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to applications
|
||||||
|
NSString *applicationsPath = @"/Applications";
|
||||||
|
NSString *newPath = [applicationsPath stringByAppendingPathComponent:@"Ollama.app"];
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
|
||||||
|
// Check if the newPath already exists
|
||||||
|
if ([fileManager fileExistsAtPath:newPath]) {
|
||||||
|
NSError *removeError = nil;
|
||||||
|
[fileManager removeItemAtPath:newPath error:&removeError];
|
||||||
|
if (removeError) {
|
||||||
|
NSLog(@"Error removing file at %@: %@", newPath, removeError);
|
||||||
|
return false; // or handle the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *moveError = nil;
|
||||||
|
[fileManager moveItemAtPath:bundlePath toPath:newPath error:&moveError];
|
||||||
|
if (moveError) {
|
||||||
|
NSLog(@"Error moving file from %@ to %@: %@", bundlePath, newPath, moveError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"Opening %@", newPath);
|
||||||
|
NSError *error = nil;
|
||||||
|
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
[workspace launchApplicationAtURL:[NSURL fileURLWithPath:newPath]
|
||||||
|
options:NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchDefault
|
||||||
|
configuration:@{}
|
||||||
|
error:&error];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int installSymlink() {
|
||||||
|
NSString *linkPath = @"/usr/local/bin/ollama";
|
||||||
|
NSError *error = nil;
|
||||||
|
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
NSString *symlinkPath = [fileManager destinationOfSymbolicLinkAtPath:linkPath error:&error];
|
||||||
|
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
||||||
|
NSString *execPath = [[NSBundle mainBundle] executablePath];
|
||||||
|
NSString *resPath = [[NSBundle mainBundle] pathForResource:@"ollama" ofType:nil];
|
||||||
|
|
||||||
|
// if the symlink already exists and points to the right place, don't prompt
|
||||||
|
if ([symlinkPath isEqualToString:resPath]) {
|
||||||
|
NSLog(@"symbolic link already exists and points to the right place");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *authorizationPrompt = @"Ollama is trying to install its command line interface (CLI) tool.";
|
||||||
|
|
||||||
|
AuthorizationRef auth = NULL;
|
||||||
|
OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
|
||||||
|
if (createStatus != errAuthorizationSuccess) {
|
||||||
|
NSLog(@"Error creating authorization");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
|
||||||
|
NSString *rightNameString = [NSString stringWithFormat:@"%@.%@", bundleIdentifier, @"auth3"];
|
||||||
|
const char *rightName = rightNameString.UTF8String;
|
||||||
|
|
||||||
|
OSStatus getRightResult = AuthorizationRightGet(rightName, NULL);
|
||||||
|
if (getRightResult == errAuthorizationDenied) {
|
||||||
|
if (AuthorizationRightSet(auth, rightName, (__bridge CFTypeRef _Nonnull)(@(kAuthorizationRuleAuthenticateAsAdmin)), (__bridge CFStringRef _Nullable)(authorizationPrompt), NULL, NULL) != errAuthorizationSuccess) {
|
||||||
|
NSLog(@"Failed to set right");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationItem right = { .name = rightName, .valueLength = 0, .value = NULL, .flags = 0 };
|
||||||
|
AuthorizationRights rights = { .count = 1, .items = &right };
|
||||||
|
AuthorizationFlags flags = (AuthorizationFlags)(kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed);
|
||||||
|
AuthorizationItem iconAuthorizationItem = {.name = kAuthorizationEnvironmentIcon, .valueLength = 0, .value = NULL, .flags = 0};
|
||||||
|
AuthorizationEnvironment authorizationEnvironment = {.count = 0, .items = NULL};
|
||||||
|
|
||||||
|
BOOL failedToUseSystemDomain = NO;
|
||||||
|
OSStatus copyStatus = AuthorizationCopyRights(auth, &rights, &authorizationEnvironment, flags, NULL);
|
||||||
|
if (copyStatus != errAuthorizationSuccess) {
|
||||||
|
failedToUseSystemDomain = YES;
|
||||||
|
|
||||||
|
if (copyStatus == errAuthorizationCanceled) {
|
||||||
|
NSLog(@"User cancelled authorization");
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
NSLog(@"Failed copying system domain rights: %d", copyStatus);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *toolPath = "/bin/ln";
|
||||||
|
const char *args[] = {"-s", "-F", [resPath UTF8String], "/usr/local/bin/ollama", NULL};
|
||||||
|
FILE *pipe = NULL;
|
||||||
|
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
OSStatus status = AuthorizationExecuteWithPrivileges(auth, toolPath, kAuthorizationFlagDefaults, (char *const *)args, &pipe);
|
||||||
|
if (status != errAuthorizationSuccess) {
|
||||||
|
NSLog(@"Failed to create symlink");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationFree(auth, kAuthorizationFlagDestroyRights);
|
||||||
|
return 0;
|
||||||
|
}
|
166
app/app_windows.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/app/lifecycle"
|
||||||
|
"github.com/ollama/ollama/app/store"
|
||||||
|
"github.com/ollama/ollama/app/tray"
|
||||||
|
"github.com/ollama/ollama/app/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppName += ".exe"
|
||||||
|
CLIName += ".exe"
|
||||||
|
// Logs, configs, downloads go to LOCALAPPDATA
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
AppDataDir = filepath.Join(localAppData, "Ollama")
|
||||||
|
AppLogFile = filepath.Join(AppDataDir, "app.log")
|
||||||
|
ServerLogFile = filepath.Join(AppDataDir, "server.log")
|
||||||
|
|
||||||
|
// Executables are stored in APPDATA
|
||||||
|
AppDir = filepath.Join(localAppData, "Programs", "Ollama")
|
||||||
|
|
||||||
|
// Make sure we have PATH set correctly for any spawned children
|
||||||
|
paths := strings.Split(os.Getenv("PATH"), ";")
|
||||||
|
// Start with whatever we find in the PATH/LD_LIBRARY_PATH
|
||||||
|
found := false
|
||||||
|
for _, path := range paths {
|
||||||
|
d, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(AppDir, d) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
paths = append(paths, AppDir)
|
||||||
|
|
||||||
|
pathVal := strings.Join(paths, ";")
|
||||||
|
slog.Debug("setting PATH=" + pathVal)
|
||||||
|
err := os.Setenv("PATH", pathVal)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to update PATH: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure our logging dir exists
|
||||||
|
_, err := os.Stat(AppDataDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(AppDataDir, 0o755); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("create ollama dir %s: %v", AppDataDir, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowLogs() {
|
||||||
|
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
||||||
|
slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir))
|
||||||
|
cmd := exec.Command(cmd_path, "/c", "start", AppDataDir)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false, CreationFlags: 0x08000000}
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Failed to open log dir: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
||||||
|
slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir))
|
||||||
|
cmd := exec.Command(cmd_path, "/c", "start", AppDataDir)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false, CreationFlags: 0x08000000}
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Failed to open log dir: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
initLogging()
|
||||||
|
|
||||||
|
slog.Info("ollama windows app started")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
var done chan int
|
||||||
|
|
||||||
|
t, err := tray.NewTray()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to start: %s", err)
|
||||||
|
}
|
||||||
|
callbacks := t.GetCallbacks()
|
||||||
|
|
||||||
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
slog.Debug("starting callback loop")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-callbacks.Quit:
|
||||||
|
slog.Debug("quit called")
|
||||||
|
t.Quit()
|
||||||
|
case <-signals:
|
||||||
|
slog.Debug("shutting down due to signal")
|
||||||
|
t.Quit()
|
||||||
|
case <-callbacks.Update:
|
||||||
|
err := updater.DoUpgrade(cancel, done)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err))
|
||||||
|
}
|
||||||
|
case <-callbacks.ShowLogs:
|
||||||
|
ShowLogs()
|
||||||
|
case <-callbacks.DoFirstUse:
|
||||||
|
err := lifecycle.GetStarted()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("Failed to launch getting started shell: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !store.GetFirstTimeRun() {
|
||||||
|
slog.Debug("First time run")
|
||||||
|
err = t.DisplayFirstUseNotification()
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug(fmt.Sprintf("XXX failed to display first use notification %v", err))
|
||||||
|
}
|
||||||
|
store.SetFirstTimeRun(true)
|
||||||
|
} else {
|
||||||
|
slog.Debug("Not first time, skipping first run notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isServerRunning(ctx) {
|
||||||
|
slog.Info("Detected another instance of ollama running, exiting")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
done, err = SpawnServer(ctx, CLIName)
|
||||||
|
if err != nil {
|
||||||
|
// TODO - should we retry in a backoff loop?
|
||||||
|
// TODO - should we pop up a warning and maybe add a menu item to view application logs?
|
||||||
|
slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err))
|
||||||
|
done = make(chan int, 1)
|
||||||
|
done <- 1
|
||||||
|
}
|
||||||
|
|
||||||
|
updater.StartBackgroundUpdaterChecker(ctx, t.UpdateAvailable)
|
||||||
|
|
||||||
|
t.Run()
|
||||||
|
cancel()
|
||||||
|
slog.Info("Waiting for ollama server to shutdown...")
|
||||||
|
if done != nil {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
slog.Info("Ollama app exiting")
|
||||||
|
}
|
40
app/darwin/Ollama.app/Contents/Info.plist
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Ollama</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>Ollama</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icon.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.ollama.ollama</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Ollama</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>DTCompiler</key>
|
||||||
|
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||||
|
<key>DTSDKBuild</key>
|
||||||
|
<string>22E245</string>
|
||||||
|
<key>DTSDKName</key>
|
||||||
|
<string>macosx13.3</string>
|
||||||
|
<key>DTXcode</key>
|
||||||
|
<string>1431</string>
|
||||||
|
<key>DTXcodeBuild</key>
|
||||||
|
<string>14E300c</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.developer-tools</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>11.0</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
BIN
app/darwin/Ollama.app/Contents/Resources/icon.png
Normal file
After Width: | Height: | Size: 382 B |
BIN
app/darwin/Ollama.app/Contents/Resources/icon@2x.png
Normal file
After Width: | Height: | Size: 691 B |
BIN
app/darwin/Ollama.app/Contents/Resources/iconDark.png
Normal file
After Width: | Height: | Size: 382 B |
BIN
app/darwin/Ollama.app/Contents/Resources/iconDark@2x.png
Normal file
After Width: | Height: | Size: 721 B |
@ -1,5 +1,3 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package lifecycle
|
package lifecycle
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
@ -1,92 +0,0 @@
|
|||||||
package lifecycle
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/ollama/ollama/app/store"
|
|
||||||
"github.com/ollama/ollama/app/tray"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Run() {
|
|
||||||
InitLogging()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
var done chan int
|
|
||||||
|
|
||||||
t, err := tray.NewTray()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to start: %s", err)
|
|
||||||
}
|
|
||||||
callbacks := t.GetCallbacks()
|
|
||||||
|
|
||||||
signals := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
slog.Debug("starting callback loop")
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-callbacks.Quit:
|
|
||||||
slog.Debug("quit called")
|
|
||||||
t.Quit()
|
|
||||||
case <-signals:
|
|
||||||
slog.Debug("shutting down due to signal")
|
|
||||||
t.Quit()
|
|
||||||
case <-callbacks.Update:
|
|
||||||
err := DoUpgrade(cancel, done)
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err))
|
|
||||||
}
|
|
||||||
case <-callbacks.ShowLogs:
|
|
||||||
ShowLogs()
|
|
||||||
case <-callbacks.DoFirstUse:
|
|
||||||
err := GetStarted()
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn(fmt.Sprintf("Failed to launch getting started shell: %s", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Are we first use?
|
|
||||||
if !store.GetFirstTimeRun() {
|
|
||||||
slog.Debug("First time run")
|
|
||||||
err = t.DisplayFirstUseNotification()
|
|
||||||
if err != nil {
|
|
||||||
slog.Debug(fmt.Sprintf("XXX failed to display first use notification %v", err))
|
|
||||||
}
|
|
||||||
store.SetFirstTimeRun(true)
|
|
||||||
} else {
|
|
||||||
slog.Debug("Not first time, skipping first run notification")
|
|
||||||
}
|
|
||||||
|
|
||||||
if IsServerRunning(ctx) {
|
|
||||||
slog.Info("Detected another instance of ollama running, exiting")
|
|
||||||
os.Exit(1)
|
|
||||||
} else {
|
|
||||||
done, err = SpawnServer(ctx, CLIName)
|
|
||||||
if err != nil {
|
|
||||||
// TODO - should we retry in a backoff loop?
|
|
||||||
// TODO - should we pop up a warning and maybe add a menu item to view application logs?
|
|
||||||
slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err))
|
|
||||||
done = make(chan int, 1)
|
|
||||||
done <- 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StartBackgroundUpdaterChecker(ctx, t.UpdateAvailable)
|
|
||||||
|
|
||||||
t.Run()
|
|
||||||
cancel()
|
|
||||||
slog.Info("Waiting for ollama server to shutdown...")
|
|
||||||
if done != nil {
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
slog.Info("Ollama app exiting")
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package lifecycle
|
|
||||||
|
|
||||||
import "log/slog"
|
|
||||||
|
|
||||||
func ShowLogs() {
|
|
||||||
slog.Warn("ShowLogs not yet implemented")
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package lifecycle
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ShowLogs() {
|
|
||||||
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
|
||||||
slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir))
|
|
||||||
cmd := exec.Command(cmd_path, "/c", "start", AppDataDir)
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false, CreationFlags: 0x08000000}
|
|
||||||
err := cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error(fmt.Sprintf("Failed to open log dir: %s", err))
|
|
||||||
}
|
|
||||||
}
|
|
@ -70,10 +70,5 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if runtime.GOOS == "darwin" {
|
|
||||||
// TODO
|
|
||||||
AppName += ".app"
|
|
||||||
// } else if runtime.GOOS == "linux" {
|
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package lifecycle
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitLogging() {
|
func initLogging() {
|
||||||
level := slog.LevelInfo
|
level := slog.LevelInfo
|
||||||
|
|
||||||
if debug := os.Getenv("OLLAMA_DEBUG"); debug != "" {
|
if debug := os.Getenv("OLLAMA_DEBUG"); debug != "" {
|
||||||
@ -41,6 +41,4 @@ func InitLogging() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
slog.SetDefault(slog.New(handler))
|
slog.SetDefault(slog.New(handler))
|
||||||
|
|
||||||
slog.Info("ollama app started")
|
|
||||||
}
|
}
|
12
app/main.go
@ -2,11 +2,15 @@ package main
|
|||||||
|
|
||||||
// Compile with the following to get rid of the cmd pop up on windows
|
// Compile with the following to get rid of the cmd pop up on windows
|
||||||
// go build -ldflags="-H windowsgui" .
|
// go build -ldflags="-H windowsgui" .
|
||||||
|
var (
|
||||||
import (
|
AppName string
|
||||||
"github.com/ollama/ollama/app/lifecycle"
|
CLIName string
|
||||||
|
AppDir string
|
||||||
|
AppDataDir string
|
||||||
|
AppLogFile string
|
||||||
|
ServerLogFile string
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
lifecycle.Run()
|
run()
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package lifecycle
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -14,65 +14,41 @@ import (
|
|||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getCLIFullPath(command string) string {
|
type ServerOptions struct {
|
||||||
cmdPath := ""
|
Cors bool
|
||||||
appExe, err := os.Executable()
|
Expose bool
|
||||||
if err == nil {
|
ModelsPath string
|
||||||
cmdPath = filepath.Join(filepath.Dir(appExe), command)
|
|
||||||
_, err := os.Stat(cmdPath)
|
|
||||||
if err == nil {
|
|
||||||
return cmdPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmdPath, err = exec.LookPath(command)
|
|
||||||
if err == nil {
|
|
||||||
_, err := os.Stat(cmdPath)
|
|
||||||
if err == nil {
|
|
||||||
return cmdPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pwd, err := os.Getwd()
|
|
||||||
if err == nil {
|
|
||||||
cmdPath = filepath.Join(pwd, command)
|
|
||||||
_, err = os.Stat(cmdPath)
|
|
||||||
if err == nil {
|
|
||||||
return cmdPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return command
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SpawnServer(ctx context.Context, command string) (chan int, error) {
|
func start(ctx context.Context, command string, options ServerOptions) (*exec.Cmd, error) {
|
||||||
done := make(chan int)
|
cmd := getCmd(ctx, command)
|
||||||
|
|
||||||
logDir := filepath.Dir(ServerLogFile)
|
// set environment variables
|
||||||
_, err := os.Stat(logDir)
|
if options.ModelsPath != "" {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
cmd.Env = append(cmd.Env, fmt.Sprintf("OLLAMA_MODELS=%s", options.ModelsPath))
|
||||||
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
}
|
||||||
return done, fmt.Errorf("create ollama server log dir %s: %v", logDir, err)
|
|
||||||
}
|
if options.Cors {
|
||||||
|
cmd.Env = append(cmd.Env, "OLLAMA_ORIGINS=*")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Expose {
|
||||||
|
cmd.Env = append(cmd.Env, "OLLAMA_HOST=0.0.0.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := getCmd(ctx, getCLIFullPath(command))
|
|
||||||
// send stdout and stderr to a file
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return done, fmt.Errorf("failed to spawn server stdout pipe %s", err)
|
return nil, fmt.Errorf("failed to spawn server stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return done, fmt.Errorf("failed to spawn server stderr pipe %s", err)
|
return nil, fmt.Errorf("failed to spawn server stderr pipe: %w", err)
|
||||||
}
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return done, fmt.Errorf("failed to spawn server stdin pipe %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - rotation
|
// TODO - rotation
|
||||||
logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return done, fmt.Errorf("failed to create server log %w", err)
|
return nil, fmt.Errorf("failed to create server log: %w", err)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer logFile.Close()
|
defer logFile.Close()
|
||||||
@ -117,19 +93,38 @@ func SpawnServer(ctx context.Context, command string) (chan int, error) {
|
|||||||
|
|
||||||
// run the command and wait for it to finish
|
// run the command and wait for it to finish
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return done, fmt.Errorf("failed to start server %w", err)
|
return nil, fmt.Errorf("failed to start server %w", err)
|
||||||
}
|
}
|
||||||
if cmd.Process != nil {
|
if cmd.Process != nil {
|
||||||
slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
|
slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
|
||||||
}
|
}
|
||||||
slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile))
|
slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile))
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SpawnServer(ctx context.Context, command string, options ServerOptions) (chan int, error) {
|
||||||
|
logDir := filepath.Dir(ServerLogFile)
|
||||||
|
_, err := os.Stat(logDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||||
|
return nil, fmt.Errorf("create ollama server log dir %s: %v", logDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan int)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// Keep the server running unless we're shuttind down the app
|
// Keep the server running unless we're shuttind down the app
|
||||||
crashCount := 0
|
crashCount := 0
|
||||||
for {
|
for {
|
||||||
|
slog.Info(fmt.Sprintf("starting server..."))
|
||||||
|
cmd, err := start(ctx, command, options)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to start server %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Wait() //nolint:errcheck
|
cmd.Wait() //nolint:errcheck
|
||||||
stdin.Close()
|
|
||||||
var code int
|
var code int
|
||||||
if cmd.ProcessState != nil {
|
if cmd.ProcessState != nil {
|
||||||
code = cmd.ProcessState.ExitCode()
|
code = cmd.ProcessState.ExitCode()
|
||||||
@ -143,19 +138,16 @@ func SpawnServer(ctx context.Context, command string) (chan int, error) {
|
|||||||
default:
|
default:
|
||||||
crashCount++
|
crashCount++
|
||||||
slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
|
slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond * time.Duration(crashCount))
|
||||||
if err := cmd.Start(); err != nil {
|
break
|
||||||
slog.Error(fmt.Sprintf("failed to restart server %s", err))
|
|
||||||
// Keep trying, but back off if we keep failing
|
|
||||||
time.Sleep(time.Duration(crashCount) * time.Second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return done, nil
|
return done, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsServerRunning(ctx context.Context) bool {
|
func isServerRunning(ctx context.Context) bool {
|
||||||
client, err := api.ClientFromEnvironment()
|
client, err := api.ClientFromEnvironment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Info("unable to connect to server")
|
slog.Info("unable to connect to server")
|
@ -1,6 +1,4 @@
|
|||||||
//go:build !windows
|
package main
|
||||||
|
|
||||||
package lifecycle
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,4 +1,4 @@
|
|||||||
package lifecycle
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,5 +1,3 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package tray
|
package tray
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,4 +1,4 @@
|
|||||||
package lifecycle
|
package updater
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -22,6 +22,10 @@ import (
|
|||||||
"github.com/ollama/ollama/version"
|
"github.com/ollama/ollama/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UpdateStageDir string
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
UpdateCheckURLBase = "https://ollama.com/api/update"
|
UpdateCheckURLBase = "https://ollama.com/api/update"
|
||||||
UpdateDownloaded = false
|
UpdateDownloaded = false
|
||||||
@ -123,7 +127,7 @@ func DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error {
|
|||||||
slog.Debug("no etag detected, falling back to filename based dedup")
|
slog.Debug("no etag detected, falling back to filename based dedup")
|
||||||
etag = "_"
|
etag = "_"
|
||||||
}
|
}
|
||||||
filename := Installer
|
filename := "OllamaSetup.exe"
|
||||||
_, params, err := mime.ParseMediaType(resp.Header.Get("content-disposition"))
|
_, params, err := mime.ParseMediaType(resp.Header.Get("content-disposition"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
filename = params["filename"]
|
filename = params["filename"]
|
@ -1,6 +1,4 @@
|
|||||||
//go:build !windows
|
package updater
|
||||||
|
|
||||||
package lifecycle
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,4 +1,4 @@
|
|||||||
package lifecycle
|
package updater
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -9,7 +9,13 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
UpdateStageDir = filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "updates")
|
||||||
|
}
|
||||||
|
|
||||||
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
||||||
|
logFile := filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "upgrade.log")
|
||||||
|
|
||||||
files, err := filepath.Glob(filepath.Join(UpdateStageDir, "*", "*.exe")) // TODO generalize for multiplatform
|
files, err := filepath.Glob(filepath.Join(UpdateStageDir, "*", "*.exe")) // TODO generalize for multiplatform
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to lookup downloads: %s", err)
|
return fmt.Errorf("failed to lookup downloads: %s", err)
|
||||||
@ -23,13 +29,13 @@ func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
|||||||
installerExe := files[0]
|
installerExe := files[0]
|
||||||
|
|
||||||
slog.Info("starting upgrade with " + installerExe)
|
slog.Info("starting upgrade with " + installerExe)
|
||||||
slog.Info("upgrade log file " + UpgradeLogFile)
|
slog.Info("upgrade log file " + logFile)
|
||||||
|
|
||||||
// When running in debug mode, we'll be "verbose" and let the installer pop up and prompt
|
// When running in debug mode, we'll be "verbose" and let the installer pop up and prompt
|
||||||
installArgs := []string{
|
installArgs := []string{
|
||||||
"/CLOSEAPPLICATIONS", // Quit the tray app if it's still running
|
"/CLOSEAPPLICATIONS", // Quit the tray app if it's still running
|
||||||
"/LOG=" + filepath.Base(UpgradeLogFile), // Only relative seems reliable, so set pwd
|
"/LOG=" + filepath.Base(logFile), // Only relative seems reliable, so set pwd
|
||||||
"/FORCECLOSEAPPLICATIONS", // Force close the tray app - might be needed
|
"/FORCECLOSEAPPLICATIONS", // Force close the tray app - might be needed
|
||||||
}
|
}
|
||||||
// When we're not in debug mode, make the upgrade as quiet as possible (no GUI, no prompts)
|
// When we're not in debug mode, make the upgrade as quiet as possible (no GUI, no prompts)
|
||||||
// TODO - temporarily disable since we're pinning in debug mode for the preview
|
// TODO - temporarily disable since we're pinning in debug mode for the preview
|
||||||
@ -53,7 +59,7 @@ func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug(fmt.Sprintf("starting installer: %s %v", installerExe, installArgs))
|
slog.Debug(fmt.Sprintf("starting installer: %s %v", installerExe, installArgs))
|
||||||
os.Chdir(filepath.Dir(UpgradeLogFile)) //nolint:errcheck
|
os.Chdir(filepath.Dir(logFile)) //nolint:errcheck
|
||||||
cmd := exec.Command(installerExe, installArgs...)
|
cmd := exec.Command(installerExe, installArgs...)
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
@ -1,5 +1,3 @@
|
|||||||
//go:build darwin
|
|
||||||
|
|
||||||
package gpu
|
package gpu
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:import/recommended",
|
|
||||||
"plugin:import/electron",
|
|
||||||
"plugin:import/typescript"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser"
|
|
||||||
}
|
|
92
macapp/.gitignore
vendored
@ -1,92 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# Webpack
|
|
||||||
.webpack/
|
|
||||||
|
|
||||||
# Vite
|
|
||||||
.vite/
|
|
||||||
|
|
||||||
# Electron-Forge
|
|
||||||
out/
|
|
@ -1,21 +0,0 @@
|
|||||||
# Desktop
|
|
||||||
|
|
||||||
This app builds upon Ollama to provide a desktop experience for running models.
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
First, build the `ollama` binary:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd ..
|
|
||||||
go build .
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the desktop app with `npm start`:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd macapp
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
Before Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 741 B |
Before Width: | Height: | Size: 440 B |
Before Width: | Height: | Size: 763 B |
Before Width: | Height: | Size: 447 B |
Before Width: | Height: | Size: 891 B |
Before Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 844 B |
@ -1,78 +0,0 @@
|
|||||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
|
||||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
|
||||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
|
||||||
import { PublisherGithub } from '@electron-forge/publisher-github'
|
|
||||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'
|
|
||||||
import { WebpackPlugin } from '@electron-forge/plugin-webpack'
|
|
||||||
import * as path from 'path'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
|
|
||||||
import { mainConfig } from './webpack.main.config'
|
|
||||||
import { rendererConfig } from './webpack.renderer.config'
|
|
||||||
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, './package.json'), 'utf8'))
|
|
||||||
|
|
||||||
const config: ForgeConfig = {
|
|
||||||
packagerConfig: {
|
|
||||||
appVersion: process.env.VERSION || packageJson.version,
|
|
||||||
asar: true,
|
|
||||||
icon: './assets/icon.icns',
|
|
||||||
extraResource: [
|
|
||||||
'../dist/ollama',
|
|
||||||
path.join(__dirname, './assets/iconTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconTemplate@2x.png'),
|
|
||||||
path.join(__dirname, './assets/iconUpdateTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconUpdateTemplate@2x.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkTemplate@2x.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkUpdateTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkUpdateTemplate@2x.png'),
|
|
||||||
],
|
|
||||||
...(process.env.SIGN
|
|
||||||
? {
|
|
||||||
osxSign: {
|
|
||||||
identity: process.env.APPLE_IDENTITY,
|
|
||||||
},
|
|
||||||
osxNotarize: {
|
|
||||||
tool: 'notarytool',
|
|
||||||
appleId: process.env.APPLE_ID || '',
|
|
||||||
appleIdPassword: process.env.APPLE_PASSWORD || '',
|
|
||||||
teamId: process.env.APPLE_TEAM_ID || '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
osxUniversal: {
|
|
||||||
x64ArchFiles: '**/ollama',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rebuildConfig: {},
|
|
||||||
makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin'])],
|
|
||||||
hooks: {
|
|
||||||
readPackageJson: async (_, packageJson) => {
|
|
||||||
return { ...packageJson, version: process.env.VERSION || packageJson.version }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new AutoUnpackNativesPlugin({}),
|
|
||||||
new WebpackPlugin({
|
|
||||||
mainConfig,
|
|
||||||
devContentSecurityPolicy: `default-src * 'unsafe-eval' 'unsafe-inline'; img-src data: 'self'`,
|
|
||||||
renderer: {
|
|
||||||
config: rendererConfig,
|
|
||||||
nodeIntegration: true,
|
|
||||||
entryPoints: [
|
|
||||||
{
|
|
||||||
html: './src/index.html',
|
|
||||||
js: './src/renderer.tsx',
|
|
||||||
name: 'main_window',
|
|
||||||
preload: {
|
|
||||||
js: './src/preload.ts',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
16695
macapp/package-lock.json
generated
@ -1,84 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ollama",
|
|
||||||
"productName": "Ollama",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "ollama",
|
|
||||||
"main": ".webpack/main",
|
|
||||||
"scripts": {
|
|
||||||
"start": "electron-forge start",
|
|
||||||
"package": "electron-forge package --arch universal",
|
|
||||||
"package:sign": "SIGN=1 electron-forge package --arch universal",
|
|
||||||
"make": "electron-forge make --arch universal",
|
|
||||||
"make:sign": "SIGN=1 electron-forge make --arch universal",
|
|
||||||
"publish": "SIGN=1 electron-forge publish",
|
|
||||||
"lint": "eslint --ext .ts,.tsx .",
|
|
||||||
"format": "prettier --check . --ignore-path .gitignore",
|
|
||||||
"format:fix": "prettier --write . --ignore-path .gitignore"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": {
|
|
||||||
"name": "Jeffrey Morgan",
|
|
||||||
"email": "jmorganca@gmail.com"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.22.5",
|
|
||||||
"@babel/preset-react": "^7.22.5",
|
|
||||||
"@electron-forge/cli": "^6.2.1",
|
|
||||||
"@electron-forge/maker-deb": "^6.2.1",
|
|
||||||
"@electron-forge/maker-rpm": "^6.2.1",
|
|
||||||
"@electron-forge/maker-squirrel": "^6.2.1",
|
|
||||||
"@electron-forge/maker-zip": "^6.2.1",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^6.2.1",
|
|
||||||
"@electron-forge/plugin-webpack": "^6.2.1",
|
|
||||||
"@electron-forge/publisher-github": "^6.2.1",
|
|
||||||
"@electron/universal": "^1.4.1",
|
|
||||||
"@svgr/webpack": "^8.0.1",
|
|
||||||
"@types/chmodr": "^1.0.0",
|
|
||||||
"@types/node": "^20.4.0",
|
|
||||||
"@types/react": "^18.2.14",
|
|
||||||
"@types/react-dom": "^18.2.6",
|
|
||||||
"@types/uuid": "^9.0.2",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
|
||||||
"@typescript-eslint/parser": "^5.60.0",
|
|
||||||
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
|
|
||||||
"babel-loader": "^9.1.2",
|
|
||||||
"chmodr": "^1.2.0",
|
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
|
||||||
"css-loader": "^6.8.1",
|
|
||||||
"electron": "25.9.2",
|
|
||||||
"eslint": "^8.43.0",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"fork-ts-checker-webpack-plugin": "^7.3.0",
|
|
||||||
"node-loader": "^2.0.0",
|
|
||||||
"postcss": "^8.4.24",
|
|
||||||
"postcss-import": "^15.1.0",
|
|
||||||
"postcss-loader": "^7.3.3",
|
|
||||||
"postcss-preset-env": "^8.5.1",
|
|
||||||
"prettier": "^2.8.8",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
|
||||||
"style-loader": "^3.3.3",
|
|
||||||
"svg-inline-loader": "^0.8.2",
|
|
||||||
"tailwindcss": "^3.3.2",
|
|
||||||
"ts-loader": "^9.4.3",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typescript": "~4.5.4",
|
|
||||||
"url-loader": "^4.1.1",
|
|
||||||
"webpack": "^5.88.0",
|
|
||||||
"webpack-cli": "^5.1.4",
|
|
||||||
"webpack-dev-server": "^4.15.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@electron/remote": "^2.0.10",
|
|
||||||
"@heroicons/react": "^2.0.18",
|
|
||||||
"@segment/analytics-node": "^1.0.0",
|
|
||||||
"copy-to-clipboard": "^3.3.3",
|
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
|
||||||
"electron-store": "^8.1.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"uuid": "^9.0.0",
|
|
||||||
"winston": "^3.10.0",
|
|
||||||
"winston-daily-rotate-file": "^4.7.1"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
'postcss-import': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag {
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-drag {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blink {
|
|
||||||
-webkit-animation: 1s blink step-end infinite;
|
|
||||||
-moz-animation: 1s blink step-end infinite;
|
|
||||||
-ms-animation: 1s blink step-end infinite;
|
|
||||||
-o-animation: 1s blink step-end infinite;
|
|
||||||
animation: 1s blink step-end infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
from,
|
|
||||||
to {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import copy from 'copy-to-clipboard'
|
|
||||||
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline'
|
|
||||||
import Store from 'electron-store'
|
|
||||||
import { getCurrentWindow, app } from '@electron/remote'
|
|
||||||
|
|
||||||
import { install } from './install'
|
|
||||||
import OllamaIcon from './ollama.svg'
|
|
||||||
|
|
||||||
const store = new Store()
|
|
||||||
|
|
||||||
enum Step {
|
|
||||||
WELCOME = 0,
|
|
||||||
CLI,
|
|
||||||
FINISH,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const [step, setStep] = useState<Step>(Step.WELCOME)
|
|
||||||
const [commandCopied, setCommandCopied] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const command = 'ollama run llama2'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='drag'>
|
|
||||||
<div className='mx-auto flex min-h-screen w-full flex-col justify-between bg-white px-4 pt-16'>
|
|
||||||
{step === Step.WELCOME && (
|
|
||||||
<>
|
|
||||||
<div className='mx-auto text-center'>
|
|
||||||
<h1 className='mb-6 mt-4 text-2xl tracking-tight text-gray-900'>Welcome to Ollama</h1>
|
|
||||||
<p className='mx-auto w-[65%] text-sm text-gray-400'>
|
|
||||||
Let's get you up and running with your own large language models.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => setStep(Step.CLI)}
|
|
||||||
className='no-drag rounded-dm mx-auto my-8 w-[40%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='mx-auto'>
|
|
||||||
<OllamaIcon />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{step === Step.CLI && (
|
|
||||||
<>
|
|
||||||
<div className='mx-auto flex flex-col space-y-28 text-center'>
|
|
||||||
<h1 className='mt-4 text-2xl tracking-tight text-gray-900'>Install the command line</h1>
|
|
||||||
<pre className='mx-auto text-4xl text-gray-400'>> ollama</pre>
|
|
||||||
<div className='mx-auto'>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await install()
|
|
||||||
setStep(Step.FINISH)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('could not install: ', e)
|
|
||||||
} finally {
|
|
||||||
getCurrentWindow().show()
|
|
||||||
getCurrentWindow().focus()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
|
||||||
>
|
|
||||||
Install
|
|
||||||
</button>
|
|
||||||
<p className='mx-auto my-4 w-[70%] text-xs text-gray-400'>
|
|
||||||
You will be prompted for administrator access
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{step === Step.FINISH && (
|
|
||||||
<>
|
|
||||||
<div className='mx-auto flex flex-col space-y-20 text-center'>
|
|
||||||
<h1 className='mt-4 text-2xl tracking-tight text-gray-900'>Run your first model</h1>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='group relative flex items-center'>
|
|
||||||
<pre className='language-none text-2xs w-full rounded-md bg-gray-100 px-4 py-3 text-start leading-normal'>
|
|
||||||
{command}
|
|
||||||
</pre>
|
|
||||||
<button
|
|
||||||
className={`no-drag absolute right-[5px] px-2 py-2 ${
|
|
||||||
commandCopied
|
|
||||||
? 'text-gray-900 opacity-100 hover:cursor-auto'
|
|
||||||
: 'text-gray-200 opacity-50 hover:cursor-pointer'
|
|
||||||
} hover:font-bold hover:text-gray-900 group-hover:opacity-100`}
|
|
||||||
onClick={() => {
|
|
||||||
copy(command)
|
|
||||||
setCommandCopied(true)
|
|
||||||
setTimeout(() => setCommandCopied(false), 3000)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{commandCopied ? (
|
|
||||||
<CheckIcon className='h-4 w-4 font-bold text-gray-500' />
|
|
||||||
) : (
|
|
||||||
<DocumentDuplicateIcon className='h-4 w-4 text-gray-500' />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className='mx-auto my-4 w-[70%] text-xs text-gray-400'>
|
|
||||||
Run this command in your favorite terminal.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
store.set('first-time-run', true)
|
|
||||||
window.close()
|
|
||||||
}}
|
|
||||||
className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
|
||||||
>
|
|
||||||
Finish
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
4
macapp/src/declarations.d.ts
vendored
@ -1,4 +0,0 @@
|
|||||||
declare module '*.svg' {
|
|
||||||
const content: string
|
|
||||||
export default content
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,302 +0,0 @@
|
|||||||
import { spawn, ChildProcess } from 'child_process'
|
|
||||||
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, MenuItemConstructorOptions, nativeTheme } from 'electron'
|
|
||||||
import Store from 'electron-store'
|
|
||||||
import winston from 'winston'
|
|
||||||
import 'winston-daily-rotate-file'
|
|
||||||
import * as path from 'path'
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { installed } from './install'
|
|
||||||
|
|
||||||
require('@electron/remote/main').initialize()
|
|
||||||
|
|
||||||
if (require('electron-squirrel-startup')) {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = new Store()
|
|
||||||
|
|
||||||
let welcomeWindow: BrowserWindow | null = null
|
|
||||||
|
|
||||||
declare const MAIN_WINDOW_WEBPACK_ENTRY: string
|
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
|
||||||
transports: [
|
|
||||||
new winston.transports.Console(),
|
|
||||||
new winston.transports.File({
|
|
||||||
filename: path.join(app.getPath('home'), '.ollama', 'logs', 'server.log'),
|
|
||||||
maxsize: 1024 * 1024 * 20,
|
|
||||||
maxFiles: 5,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
format: winston.format.printf(info => info.message),
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('ready', () => {
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.exit(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('second-instance', () => {
|
|
||||||
if (app.hasSingleInstanceLock()) {
|
|
||||||
app.releaseSingleInstanceLock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proc) {
|
|
||||||
proc.off('exit', restart)
|
|
||||||
proc.kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.focus({ steal: true })
|
|
||||||
|
|
||||||
init()
|
|
||||||
})
|
|
||||||
|
|
||||||
function firstRunWindow() {
|
|
||||||
// Create the browser window.
|
|
||||||
welcomeWindow = new BrowserWindow({
|
|
||||||
width: 400,
|
|
||||||
height: 500,
|
|
||||||
frame: false,
|
|
||||||
fullscreenable: false,
|
|
||||||
resizable: false,
|
|
||||||
movable: true,
|
|
||||||
show: false,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require('@electron/remote/main').enable(welcomeWindow.webContents)
|
|
||||||
|
|
||||||
welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
|
|
||||||
welcomeWindow.on('ready-to-show', () => welcomeWindow.show())
|
|
||||||
welcomeWindow.on('closed', () => {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.dock.hide()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let tray: Tray | null = null
|
|
||||||
let updateAvailable = false
|
|
||||||
const assetPath = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..', '..', 'assets')
|
|
||||||
|
|
||||||
function trayIconPath() {
|
|
||||||
return nativeTheme.shouldUseDarkColors
|
|
||||||
? updateAvailable
|
|
||||||
? path.join(assetPath, 'iconDarkUpdateTemplate.png')
|
|
||||||
: path.join(assetPath, 'iconDarkTemplate.png')
|
|
||||||
: updateAvailable
|
|
||||||
? path.join(assetPath, 'iconUpdateTemplate.png')
|
|
||||||
: path.join(assetPath, 'iconTemplate.png')
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTrayIcon() {
|
|
||||||
if (tray) {
|
|
||||||
tray.setImage(trayIconPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTray() {
|
|
||||||
const updateItems: MenuItemConstructorOptions[] = [
|
|
||||||
{ label: 'An update is available', enabled: false },
|
|
||||||
{
|
|
||||||
label: 'Restart to update',
|
|
||||||
click: () => autoUpdater.quitAndInstall(),
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate([
|
|
||||||
...(updateAvailable ? updateItems : []),
|
|
||||||
{ role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' },
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!tray) {
|
|
||||||
tray = new Tray(trayIconPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
tray.setToolTip(updateAvailable ? 'An update is available' : 'Ollama')
|
|
||||||
tray.setContextMenu(menu)
|
|
||||||
tray.setImage(trayIconPath())
|
|
||||||
|
|
||||||
nativeTheme.off('updated', updateTrayIcon)
|
|
||||||
nativeTheme.on('updated', updateTrayIcon)
|
|
||||||
}
|
|
||||||
|
|
||||||
let proc: ChildProcess = null
|
|
||||||
|
|
||||||
function server() {
|
|
||||||
const binary = app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, 'ollama')
|
|
||||||
: path.resolve(process.cwd(), '..', 'ollama')
|
|
||||||
|
|
||||||
proc = spawn(binary, ['serve'])
|
|
||||||
|
|
||||||
proc.stdout.on('data', data => {
|
|
||||||
logger.info(data.toString().trim())
|
|
||||||
})
|
|
||||||
|
|
||||||
proc.stderr.on('data', data => {
|
|
||||||
logger.error(data.toString().trim())
|
|
||||||
})
|
|
||||||
|
|
||||||
proc.on('exit', restart)
|
|
||||||
}
|
|
||||||
|
|
||||||
function restart() {
|
|
||||||
setTimeout(server, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
if (proc) {
|
|
||||||
proc.off('exit', restart)
|
|
||||||
proc.kill('SIGINT') // send SIGINT signal to the server, which also stops any loaded llms
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateURL = `https://ollama.ai/api/update?os=${process.platform}&arch=${
|
|
||||||
process.arch
|
|
||||||
}&version=${app.getVersion()}&id=${id()}`
|
|
||||||
|
|
||||||
let latest = ''
|
|
||||||
async function isNewReleaseAvailable() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(updateURL)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
const url = data?.url
|
|
||||||
if (!url) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latest === url) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
latest = url
|
|
||||||
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`update check failed - ${error}`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUpdate() {
|
|
||||||
const available = await isNewReleaseAvailable()
|
|
||||||
if (available) {
|
|
||||||
logger.info('checking for update')
|
|
||||||
autoUpdater.checkForUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (app.isPackaged) {
|
|
||||||
checkUpdate()
|
|
||||||
setInterval(() => {
|
|
||||||
checkUpdate()
|
|
||||||
}, 60 * 60 * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTray()
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
if (app.isPackaged) {
|
|
||||||
if (!app.isInApplicationsFolder()) {
|
|
||||||
const chosen = dialog.showMessageBoxSync({
|
|
||||||
type: 'question',
|
|
||||||
buttons: ['Move to Applications', 'Do Not Move'],
|
|
||||||
message: 'Ollama works best when run from the Applications directory.',
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (chosen === 0) {
|
|
||||||
try {
|
|
||||||
app.moveToApplicationsFolder({
|
|
||||||
conflictHandler: conflictType => {
|
|
||||||
if (conflictType === 'existsAndRunning') {
|
|
||||||
dialog.showMessageBoxSync({
|
|
||||||
type: 'info',
|
|
||||||
message: 'Cannot move to Applications directory',
|
|
||||||
detail:
|
|
||||||
'Another version of Ollama is currently running from your Applications directory. Close it first and try again.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`[Move to Applications] Failed to move to applications folder - ${e.message}}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server()
|
|
||||||
|
|
||||||
if (store.get('first-time-run') && installed()) {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.dock.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the first run or the CLI is no longer installed
|
|
||||||
app.setLoginItemSettings({ openAtLogin: true })
|
|
||||||
firstRunWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
|
||||||
// for applications and their menu bar to stay active until the user quits
|
|
||||||
// explicitly with Cmd + Q.
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function id(): string {
|
|
||||||
const id = store.get('id') as string
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
const uuid = uuidv4()
|
|
||||||
store.set('id', uuid)
|
|
||||||
return uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdater.setFeedURL({ url: updateURL })
|
|
||||||
|
|
||||||
autoUpdater.on('error', e => {
|
|
||||||
logger.error(`update check failed - ${e.message}`)
|
|
||||||
console.error(`update check failed - ${e.message}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', () => {
|
|
||||||
updateAvailable = true
|
|
||||||
updateTray()
|
|
||||||
})
|
|
@ -1,21 +0,0 @@
|
|||||||
import * as fs from 'fs'
|
|
||||||
import { exec as cbExec } from 'child_process'
|
|
||||||
import * as path from 'path'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
|
|
||||||
const app = process && process.type === 'renderer' ? require('@electron/remote').app : require('electron').app
|
|
||||||
const ollama = app.isPackaged ? path.join(process.resourcesPath, 'ollama') : path.resolve(process.cwd(), '..', 'ollama')
|
|
||||||
const exec = promisify(cbExec)
|
|
||||||
const symlinkPath = '/usr/local/bin/ollama'
|
|
||||||
|
|
||||||
export function installed() {
|
|
||||||
return fs.existsSync(symlinkPath) && fs.readlinkSync(symlinkPath) === ollama
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function install() {
|
|
||||||
const command = `do shell script "mkdir -p ${path.dirname(
|
|
||||||
symlinkPath
|
|
||||||
)} && ln -F -s \\"${ollama}\\" \\"${symlinkPath}\\"" with administrator privileges`
|
|
||||||
|
|
||||||
await exec(`osascript -e '${command}'`)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 17 KiB |
@ -1,7 +0,0 @@
|
|||||||
import App from './app'
|
|
||||||
import './app.css'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
|
|
||||||
const container = document.getElementById('app')
|
|
||||||
const root = createRoot(container)
|
|
||||||
root.render(<App />)
|
|
@ -1,6 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
|
||||||
theme: {},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES6",
|
|
||||||
"allowJs": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"paths": {
|
|
||||||
"*": ["node_modules/*"]
|
|
||||||
},
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import type { Configuration } from 'webpack'
|
|
||||||
|
|
||||||
import { rules } from './webpack.rules'
|
|
||||||
import { plugins } from './webpack.plugins'
|
|
||||||
|
|
||||||
export const mainConfig: Configuration = {
|
|
||||||
/**
|
|
||||||
* This is the main entry point for your application, it's the first file
|
|
||||||
* that runs in the main process.
|
|
||||||
*/
|
|
||||||
entry: './src/index.ts',
|
|
||||||
// Put your normal webpack config below here
|
|
||||||
module: {
|
|
||||||
rules,
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
|
|
||||||
import { DefinePlugin } from 'webpack'
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
|
|
||||||
|
|
||||||
export const plugins = [
|
|
||||||
new ForkTsCheckerWebpackPlugin({
|
|
||||||
logger: 'webpack-infrastructure',
|
|
||||||
}),
|
|
||||||
new DefinePlugin({
|
|
||||||
'process.env.TELEMETRY_WRITE_KEY': JSON.stringify(process.env.TELEMETRY_WRITE_KEY),
|
|
||||||
}),
|
|
||||||
]
|
|
@ -1,19 +0,0 @@
|
|||||||
import type { Configuration } from 'webpack'
|
|
||||||
|
|
||||||
import { rules } from './webpack.rules'
|
|
||||||
import { plugins } from './webpack.plugins'
|
|
||||||
|
|
||||||
rules.push({
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' }],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const rendererConfig: Configuration = {
|
|
||||||
module: {
|
|
||||||
rules,
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
import type { ModuleOptions } from 'webpack'
|
|
||||||
|
|
||||||
export const rules: Required<ModuleOptions>['rules'] = [
|
|
||||||
// Add support for native node modules
|
|
||||||
{
|
|
||||||
// We're specifying native_modules in the test because the asset relocator loader generates a
|
|
||||||
// "fake" .node file which is really a cjs file.
|
|
||||||
test: /native_modules[/\\].+\.node$/,
|
|
||||||
use: 'node-loader',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
|
|
||||||
parser: { amd: false },
|
|
||||||
use: {
|
|
||||||
loader: '@vercel/webpack-asset-relocator-loader',
|
|
||||||
options: {
|
|
||||||
outputAssetBase: 'native_modules',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
exclude: /(node_modules|\.webpack)/,
|
|
||||||
use: {
|
|
||||||
loader: 'ts-loader',
|
|
||||||
options: {
|
|
||||||
transpileOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: ['@svgr/webpack'],
|
|
||||||
},
|
|
||||||
]
|
|
@ -11,26 +11,37 @@ for TARGETARCH in arm64 amd64; do
|
|||||||
rm -rf llm/llama.cpp/build
|
rm -rf llm/llama.cpp/build
|
||||||
GOOS=darwin GOARCH=$TARGETARCH go generate ./...
|
GOOS=darwin GOARCH=$TARGETARCH go generate ./...
|
||||||
CGO_ENABLED=1 GOOS=darwin GOARCH=$TARGETARCH go build -trimpath -o dist/ollama-darwin-$TARGETARCH
|
CGO_ENABLED=1 GOOS=darwin GOARCH=$TARGETARCH go build -trimpath -o dist/ollama-darwin-$TARGETARCH
|
||||||
CGO_ENABLED=1 GOOS=darwin GOARCH=$TARGETARCH go build -trimpath -cover -o dist/ollama-darwin-$TARGETARCH-cov
|
CGO_ENABLED=1 GOOS=darwin GOARCH=$TARGETARCH go build -C app -trimpath -o ../dist/ollama-app-darwin-$TARGETARCH
|
||||||
done
|
done
|
||||||
|
|
||||||
lipo -create -output dist/ollama dist/ollama-darwin-arm64 dist/ollama-darwin-amd64
|
lipo -create -output dist/ollama dist/ollama-darwin-arm64 dist/ollama-darwin-amd64
|
||||||
rm -f dist/ollama-darwin-arm64 dist/ollama-darwin-amd64
|
lipo -create -output dist/ollama-app dist/ollama-app-darwin-arm64 dist/ollama-app-darwin-amd64
|
||||||
|
rm -f dist/ollama-darwin-* dist/ollama-app-darwin-*
|
||||||
|
|
||||||
|
# create the mac app
|
||||||
|
rm -rf dist/Ollama.app
|
||||||
|
cp -R app/darwin/Ollama.app dist/
|
||||||
|
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" dist/Ollama.app/Contents/Info.plist
|
||||||
|
mkdir -p dist/Ollama.app/Contents/MacOS
|
||||||
|
mv dist/ollama-app dist/Ollama.app/Contents/MacOS/Ollama
|
||||||
|
cp dist/ollama dist/Ollama.app/Contents/Resources/ollama
|
||||||
|
|
||||||
|
# sign and notarize the app
|
||||||
if [ -n "$APPLE_IDENTITY" ]; then
|
if [ -n "$APPLE_IDENTITY" ]; then
|
||||||
codesign --deep --force --options=runtime --sign "$APPLE_IDENTITY" --timestamp dist/ollama
|
codesign -f --timestamp --options=runtime --sign "$APPLE_IDENTITY" --identifier ai.ollama.ollama dist/Ollama.app/Contents/MacOS/Ollama
|
||||||
|
codesign -f --timestamp --options=runtime --sign "$APPLE_IDENTITY" --identifier ai.ollama.ollama dist/Ollama.app/Contents/Resources/ollama
|
||||||
|
codesign -f --timestamp --options=runtime --sign "$APPLE_IDENTITY" --identifier ai.ollama.ollama dist/Ollama.app
|
||||||
|
ditto -c -k --keepParent dist/Ollama.app dist/Ollama-darwin.zip
|
||||||
|
rm -rf dist/Ollama.app
|
||||||
|
xcrun notarytool submit dist/Ollama-darwin.zip --wait --timeout 10m --apple-id $APPLE_ID --password $APPLE_PASSWORD --team-id $APPLE_TEAM_ID
|
||||||
|
unzip dist/Ollama-darwin.zip -d dist
|
||||||
|
rm -f dist/Ollama-darwin.zip
|
||||||
|
xcrun stapler staple "dist/Ollama.app"
|
||||||
|
ditto -c -k --keepParent dist/Ollama.app dist/Ollama-darwin.zip
|
||||||
|
rm -rf dist/Ollama.app
|
||||||
else
|
else
|
||||||
echo "Skipping code signing - set APPLE_IDENTITY"
|
echo "Skipping code signing - set APPLE_IDENTITY"
|
||||||
fi
|
fi
|
||||||
chmod +x dist/ollama
|
|
||||||
|
|
||||||
# build and optionally sign the mac app
|
|
||||||
npm install --prefix macapp
|
|
||||||
if [ -n "$APPLE_IDENTITY" ]; then
|
|
||||||
npm run --prefix macapp make:sign
|
|
||||||
else
|
|
||||||
npm run --prefix macapp make
|
|
||||||
fi
|
|
||||||
cp macapp/out/make/zip/darwin/universal/Ollama-darwin-universal-$VERSION.zip dist/Ollama-darwin.zip
|
|
||||||
|
|
||||||
# sign the binary and rename it
|
# sign the binary and rename it
|
||||||
if [ -n "$APPLE_IDENTITY" ]; then
|
if [ -n "$APPLE_IDENTITY" ]; then
|
||||||
|
@ -88,8 +88,8 @@ function buildOllama() {
|
|||||||
function buildApp() {
|
function buildApp() {
|
||||||
write-host "Building Ollama App"
|
write-host "Building Ollama App"
|
||||||
cd "${script:SRC_DIR}\app"
|
cd "${script:SRC_DIR}\app"
|
||||||
& windres -l 0 -o ollama.syso ollama.rc
|
& windres -l 0 -o ollama.syso windows\ollama.rc
|
||||||
& go build -trimpath -ldflags "-s -w -H windowsgui -X=github.com/ollama/ollama/version.Version=$script:VERSION -X=github.com/ollama/ollama/server.mode=release" .
|
& go build -trimpath -ldflags "-s -w -H windowsgui -X=github.com/jmorganca/ollama/version.Version=$script:VERSION -X=github.com/jmorganca/ollama/server.mode=release" .
|
||||||
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
if ("${env:KEY_CONTAINER}") {
|
if ("${env:KEY_CONTAINER}") {
|
||||||
& "${script:SignTool}" sign /v /fd sha256 /t http://timestamp.digicert.com /f "${script:OLLAMA_CERT}" `
|
& "${script:SignTool}" sign /v /fd sha256 /t http://timestamp.digicert.com /f "${script:OLLAMA_CERT}" `
|
||||||
@ -110,7 +110,7 @@ function gatherDependencies() {
|
|||||||
cp "${env:VCToolsRedistDir}\x64\Microsoft.VC*.CRT\vcruntime140_1.dll" "${script:DEPS_DIR}\"
|
cp "${env:VCToolsRedistDir}\x64\Microsoft.VC*.CRT\vcruntime140_1.dll" "${script:DEPS_DIR}\"
|
||||||
|
|
||||||
|
|
||||||
cp "${script:SRC_DIR}\app\ollama_welcome.ps1" "${script:SRC_DIR}\dist\"
|
cp "${script:SRC_DIR}\app\windows\ollama_welcome.ps1" "${script:SRC_DIR}\dist\"
|
||||||
if ("${env:KEY_CONTAINER}") {
|
if ("${env:KEY_CONTAINER}") {
|
||||||
write-host "about to sign"
|
write-host "about to sign"
|
||||||
foreach ($file in (get-childitem "${script:DEPS_DIR}/cu*.dll") + @("${script:SRC_DIR}\dist\ollama_welcome.ps1")){
|
foreach ($file in (get-childitem "${script:DEPS_DIR}/cu*.dll") + @("${script:SRC_DIR}\dist\ollama_welcome.ps1")){
|
||||||
@ -127,9 +127,9 @@ function buildInstaller() {
|
|||||||
cd "${script:SRC_DIR}\app"
|
cd "${script:SRC_DIR}\app"
|
||||||
$env:PKG_VERSION=$script:PKG_VERSION
|
$env:PKG_VERSION=$script:PKG_VERSION
|
||||||
if ("${env:KEY_CONTAINER}") {
|
if ("${env:KEY_CONTAINER}") {
|
||||||
& "${script:INNO_SETUP_DIR}\ISCC.exe" /SMySignTool="${script:SignTool} sign /fd sha256 /t http://timestamp.digicert.com /f ${script:OLLAMA_CERT} /csp `$qGoogle Cloud KMS Provider`$q /kc ${env:KEY_CONTAINER} `$f" .\ollama.iss
|
& "${script:INNO_SETUP_DIR}\ISCC.exe" /SMySignTool="${script:SignTool} sign /fd sha256 /t http://timestamp.digicert.com /f ${script:OLLAMA_CERT} /csp `$qGoogle Cloud KMS Provider`$q /kc ${env:KEY_CONTAINER} `$f" .\windows\ollama.iss
|
||||||
} else {
|
} else {
|
||||||
& "${script:INNO_SETUP_DIR}\ISCC.exe" .\ollama.iss
|
& "${script:INNO_SETUP_DIR}\ISCC.exe" .\windows\ollama.iss
|
||||||
}
|
}
|
||||||
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
# Set your variables here.
|
|
||||||
REPO="jmorganca/ollama"
|
|
||||||
|
|
||||||
# Check if VERSION is set
|
|
||||||
if [[ -z "${VERSION}" ]]; then
|
|
||||||
echo "VERSION is not set. Please set the VERSION environment variable."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
OS=$(go env GOOS)
|
|
||||||
|
|
||||||
./script/build_${OS}.sh
|
|
||||||
|
|
||||||
# Create a new tag if it doesn't exist.
|
|
||||||
if ! git rev-parse v$VERSION >/dev/null 2>&1; then
|
|
||||||
git tag v$VERSION
|
|
||||||
fi
|
|
||||||
|
|
||||||
git push origin v$VERSION
|
|
||||||
|
|
||||||
# Create a new release.
|
|
||||||
gh release create -p v$VERSION -t v$VERSION
|
|
||||||
|
|
||||||
# Upload the zip file.
|
|
||||||
gh release upload v$VERSION ./dist/* --clobber
|
|
10
scripts/run_darwin.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rm -rf $TMPDIR/Ollama.app
|
||||||
|
cp -R app/darwin/Ollama.app $TMPDIR/Ollama.app
|
||||||
|
mkdir -p $TMPDIR/Ollama.app/Contents/Resources $TMPDIR/Ollama.app/Contents/MacOS
|
||||||
|
go build -o $TMPDIR/Ollama.app/Contents/Resources/ollama .
|
||||||
|
go build -C app -o $TMPDIR/Ollama.app/Contents/MacOS/Ollama .
|
||||||
|
$TMPDIR/Ollama.app/Contents/MacOS/Ollama
|