package main import ( "context" "database/sql" _ "embed" "flag" "fmt" "os" "os/signal" "path/filepath" "runtime/pprof" "strings" "time" _ "modernc.org/sqlite" "codexis/db" "codexis/indexer" ) //go:embed db/schema.sql var schemaSQL string const dbDir = ".codexis" const dbFileName = "index.db" func main() { force := flag.Bool("force", false, "Force full re-index (ignore file hashes)") output := flag.String("o", "", "Output database path (default: /.codexis/index.db)") cpuprofile := flag.String("cpuprofile", "", "Write CPU profile to file") flag.Parse() // CPU profiling if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { fmt.Fprintf(os.Stderr, "error creating profile: %v\n", err) os.Exit(1) } pprof.StartCPUProfile(f) // Flush profile on interrupt sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) go func() { <-sigCh fmt.Fprintf(os.Stderr, "\nInterrupted, flushing CPU profile...\n") pprof.StopCPUProfile() f.Close() os.Exit(1) }() defer func() { pprof.StopCPUProfile() f.Close() }() } root := "." if flag.NArg() > 0 { root = flag.Arg(0) } absRoot, err := filepath.Abs(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } dbDirPath := filepath.Join(absRoot, dbDir) if err := os.MkdirAll(dbDirPath, 0755); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } dbPath := filepath.Join(dbDirPath, dbFileName) if *output != "" { dbPath = *output } if err := run(absRoot, dbPath, *force); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func run(root, dbPath string, force bool) error { ctx := context.Background() sqlDB, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)") if err != nil { return fmt.Errorf("opening database: %w", err) } defer sqlDB.Close() // Create schema if err := createSchema(ctx, sqlDB); err != nil { return fmt.Errorf("creating schema: %w", err) } queries := db.New(sqlDB) idx := indexer.New(sqlDB, queries, root, force) isTTY := fileIsTTY(os.Stderr) idx.OnProgress = func(current, total int, path string) { if !isTTY { return } pct := current * 100 / total barWidth := 30 filled := barWidth * current / total bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled) display := path if len(display) > 40 { display = "..." + display[len(display)-37:] } fmt.Fprintf(os.Stderr, "\r %s %3d%% (%d/%d) %s\033[K", bar, pct, current, total, display) if current == total { fmt.Fprintf(os.Stderr, "\r\033[K") } } start := time.Now() fmt.Fprintf(os.Stderr, "Indexing %s...\n", root) stats, err := idx.Index(ctx) if err != nil { return fmt.Errorf("indexing: %w", err) } elapsed := time.Since(start) fmt.Fprintf(os.Stderr, "Done in %s\n", elapsed.Round(time.Millisecond)) fmt.Fprintf(os.Stderr, " Files: %d total, %d indexed, %d unchanged\n", stats.FilesTotal, stats.FilesIndexed, stats.FilesSkipped) fmt.Fprintf(os.Stderr, " Symbols: %d\n", stats.SymbolsTotal) fmt.Fprintf(os.Stderr, " Output: %s\n", dbPath) return nil } func fileIsTTY(f *os.File) bool { fi, err := f.Stat() if err != nil { return false } return fi.Mode()&os.ModeCharDevice != 0 } func createSchema(ctx context.Context, sqlDB *sql.DB) error { _, err := sqlDB.ExecContext(ctx, schemaSQL) return err }