From 6d89432b670d66df48d6b1178d49f571b942fe44 Mon Sep 17 00:00:00 2001 From: "Magomed Kostoev (mkostoevr)" Date: Thu, 2 Dec 2021 20:01:38 +0000 Subject: [PATCH] [WS] Implement basic workspace system * get_started.py sets up a new workspace in a project folder * build.py builds the project using info from Tupfile.lua * run.py moves compiled program into kolibri.img and boots qemu on it Only supports very simple programs for now (tested on few demos). Only tested on Windows for now. git-svn-id: svn://kolibrios.org@9357 a494cfbc-eb01-0410-851d-a64ba20cac60 --- _tools/get_started.py | 34 ++ _tools/lib/makeflop.py | 792 +++++++++++++++++++++++++++++++++++ _tools/lib/tupfile_parser.py | 153 +++++++ _tools/workspace/build.py | 24 ++ _tools/workspace/run.py | 103 +++++ 5 files changed, 1106 insertions(+) create mode 100644 _tools/get_started.py create mode 100644 _tools/lib/makeflop.py create mode 100644 _tools/lib/tupfile_parser.py create mode 100644 _tools/workspace/build.py create mode 100644 _tools/workspace/run.py diff --git a/_tools/get_started.py b/_tools/get_started.py new file mode 100644 index 0000000000..e1edbceeb5 --- /dev/null +++ b/_tools/get_started.py @@ -0,0 +1,34 @@ +#!/usr/bin/python3 +# Copyright Magomed Kostoev +# Published under MIT license + +import os + +def log(s, end = "\n"): + print(s, end = end, flush = True) + +def install_python_script(src, dst, tools_lib): + log(f"Copying {src}... ", end = "") + + with open(src) as src_file: + script = src_file.read() + tools_lib_escaped = tools_lib.replace("\\", "\\\\") + repl_from = "path_to_lib = '../lib'" + repl_to = f"path_to_lib ='{tools_lib_escaped}'" + script = script.replace(repl_from, repl_to, 1) + with open(dst, "w") as dst_file: + dst_file.write(script) + + log(f"Done") + +if __name__ == "__main__": + tools_get_started_py = os.path.abspath(__file__) + tools = os.sep.join(tools_get_started_py.split(os.sep)[:-1]) + tools_lib = os.sep.join([tools, "lib"]) + tools_workspace = os.sep.join([tools, "workspace"]) + # Copy scripts from _tools/workspace to current folder, but let them know + # where the _tools/lib is (change their value of tools_lib variable) + tools_workspace_run_py = os.sep.join([tools_workspace, "run.py"]) + tools_workspace_build_py = os.sep.join([tools_workspace, "build.py"]) + install_python_script(tools_workspace_run_py, "run.py", tools_lib) + install_python_script(tools_workspace_build_py, "build.py", tools_lib) diff --git a/_tools/lib/makeflop.py b/_tools/lib/makeflop.py new file mode 100644 index 0000000000..1a4b53a7d8 --- /dev/null +++ b/_tools/lib/makeflop.py @@ -0,0 +1,792 @@ +#!/usr/bin/env python3 +# +# makeflop.py +# Version 3 +# Brad Smith, 2019 +# http://rainwarrior.ca +# +# Simple file operations for a FAT12 floppy disk image. +# Public domain. + +import sys +assert sys.version_info[0] == 3, "Python 3 required." + +import struct +import datetime +import os + +class Floppy: + """ + Simple file operations for a FAT12 floppy disk image. + + Floppy() - creates a blank DOS formatted 1.44MB disk. + Floppy(data) - parses a disk from a given array of bytes. + .open(filename) - loads a file and returns a Floppy from its data. + .save(filename) - saves the disk image to a file. + .flush() - Updates the .data member with any pending changes. + .data - A bytearray of the disk image. + + .files() - Returns a list of strings, each is a file or directory. Directories end with a backslash. + .delete_path(path) - Deletes a file or directory (recursive). + .add_dir_path(path) - Creates a new empty directory, if it does not already exist (recursive). Returns cluster of directory, or -1 if failed. + .add_file_path(path,data) - Creates a new file (creating directory if needed) with the given data. Returns False if failed. + .extract_file_path(path) - Returns a bytearray of the file at the given path, None if not found. + .set_volume_id(id=None) - Sets the 32-bit volume ID. Use with no arguments to generate ID from current time. + .set_volume_label(label) - Sets the 11-character volume label. + + .boot_info() - Returns a string displaying boot sector information. + .fat_info() - Returns a very long string of all 12-bit FAT entries. + .files_info() - Returns a string displaying the files() list. + + .add_all(path,prefix) - Adds all files from local path to disk (uppercased). Use prefix to specify a target directory. Returns False if any failed. + .extract_all(path) - Dumps entire contents of disk to local path. + + .find_path(path) - Returns a Floppy.FileEntry. + FileEntry.info() - Returns and information string about a FileEntry. + + This class provides some basic interface to a FAT12 floppy disk image. + Some things are fragile, in particular filenames that are too long, + or references to clusters outside the disk may cause exceptions. + The FAT can be accessed directly with some internal functions (see implementation) + and changes will be applied to the stored .data image with .flush(). + + Example: + f = Floppy.open("disk.img") + print(f.boot_info()) # list boot information about disk + print(f.file_info()) # list files and directories + f.extract_all("disk_dump\\") + f.delete_path("DIR\\FILE.TXT") # delete a file + f.delete_path("DIR") # delete an entire directory + f.add_file_path("DIR\\NEW.TXT",open("new.txt","rb").read()) # add a file, directory automatically created + f.add_dir_path("NEWDIR") # creates a new directory + f.add_all("add\\","ADDED\\") # adds all files from a local directory to to a specified floppy directory. + f.set_volume_id() # generates a new volume ID + f.set_volume_label("MY DISK") # changes the volume label + f.save("new.img") + """ + + EMPTY = 0xE5 # incipit value for an empty directory entry + + def _filestring(s, length): + """Creates an ASCII string, padded to length with spaces ($20).""" + b = bytearray(s.encode("ASCII")) + b = b + bytearray([0x20] * (length - len(b))) + if len(b) != length: + raise self.Error("File string '%s' too long? (%d != %d)" % (s,len(b),length)) + return b + + class Error(Exception): + """Floppy has had an error.""" + pass + + class FileEntry: + """A directory entry for a file.""" + + def __init__(self, data=None, dir_cluster=-1, dir_index=-1): + """Unpacks a 32 byte directory entry into a FileEntry structure.""" + if data == None: + data = bytearray([Floppy.EMPTY]+([0]*31)) + self.data = bytearray(data) + self.path = "" + self.incipit = data[0] + if self.incipit != 0x00 and self.incipit != Floppy.EMPTY: + filename = data[0:8].decode("cp866") + filename = filename.rstrip(" ") + extension = data[8:11].decode("cp866") + extension = extension.rstrip(" ") + self.path = filename + if len(extension) > 0: + self.path = self.path + "." + extension + block = struct.unpack("= 0): + filename = self.path[0:period] + extension = self.path[period+1:] + else: + filename = self.path + if self.incipit != 0x00 and self.incipit != 0xEF: + self.data[0:8] = Floppy._filestring(filename,8) + self.data[8:11] = Floppy._filestring(extension,3) + else: + self.data[0] = self.incipit + self.data[11] = self.attributes + self.data[22:32] = bytearray(struct.pack("> 1) + return (date, time) + + def fat_time_now(): + """Builds the current time as a FAT12 date/time entry.""" + now = datetime.datetime.now() + return Floppy.FileEntry.fat_time(now.year, now.month, now.day, now.hour, now.minute, now.second) + + def set_name(self,s): + """Sets the filename and updates incipit.""" + self.path = s + self.incipit = Floppy._filestring(s,12)[0] + + def set_now(self): + """Updates modified date/time to now.""" + (date,time) = Floppy.FileEntry.fat_time_now() + self.write_date = date + self.write_time = time + + def new_file(name): + """Generate a new file entry.""" + e = Floppy.FileEntry() + e.set_name(name) + e.set_now() + e.attributes = 0x00 + return e + + def new_dir(name): + """Generate a new subdirectory entry.""" + e = Floppy.FileEntry() + e.set_name(name) + e.set_now() + e.attributes = 0x10 + return e + + def new_volume(name): + """Generate a new volume entry.""" + e = Floppy.FileEntry() + if len(name) > 8: + name = name[0:8] + "." + name[8:] + e.set_name(name) + e.set_now() + e.attributes = 0x08 + return e + + def new_terminal(): + """Generate a new directory terminating entry.""" + e = Floppy.FileEntry() + e.incipit = 0x00 + return e + + # blank formatted 1.44MB MS-DOS 5.0 non-system floppy + blank_floppy = [ # boot sector, 2xFAT, "BLANK" volume label, F6 fill + 0xEB,0x3C,0x90,0x4D,0x53,0x44,0x4F,0x53,0x35,0x2E,0x30,0x00,0x02,0x01,0x01,0x00, + 0x02,0xE0,0x00,0x40,0x0B,0xF0,0x09,0x00,0x12,0x00,0x02,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x29,0xE3,0x16,0x53,0x1B,0x42,0x4C,0x41,0x4E,0x4B, + 0x20,0x20,0x20,0x20,0x20,0x20,0x46,0x41,0x54,0x31,0x32,0x20,0x20,0x20,0xFA,0x33, + 0xC0,0x8E,0xD0,0xBC,0x00,0x7C,0x16,0x07,0xBB,0x78,0x00,0x36,0xC5,0x37,0x1E,0x56, + 0x16,0x53,0xBF,0x3E,0x7C,0xB9,0x0B,0x00,0xFC,0xF3,0xA4,0x06,0x1F,0xC6,0x45,0xFE, + 0x0F,0x8B,0x0E,0x18,0x7C,0x88,0x4D,0xF9,0x89,0x47,0x02,0xC7,0x07,0x3E,0x7C,0xFB, + 0xCD,0x13,0x72,0x79,0x33,0xC0,0x39,0x06,0x13,0x7C,0x74,0x08,0x8B,0x0E,0x13,0x7C, + 0x89,0x0E,0x20,0x7C,0xA0,0x10,0x7C,0xF7,0x26,0x16,0x7C,0x03,0x06,0x1C,0x7C,0x13, + 0x16,0x1E,0x7C,0x03,0x06,0x0E,0x7C,0x83,0xD2,0x00,0xA3,0x50,0x7C,0x89,0x16,0x52, + 0x7C,0xA3,0x49,0x7C,0x89,0x16,0x4B,0x7C,0xB8,0x20,0x00,0xF7,0x26,0x11,0x7C,0x8B, + 0x1E,0x0B,0x7C,0x03,0xC3,0x48,0xF7,0xF3,0x01,0x06,0x49,0x7C,0x83,0x16,0x4B,0x7C, + 0x00,0xBB,0x00,0x05,0x8B,0x16,0x52,0x7C,0xA1,0x50,0x7C,0xE8,0x92,0x00,0x72,0x1D, + 0xB0,0x01,0xE8,0xAC,0x00,0x72,0x16,0x8B,0xFB,0xB9,0x0B,0x00,0xBE,0xE6,0x7D,0xF3, + 0xA6,0x75,0x0A,0x8D,0x7F,0x20,0xB9,0x0B,0x00,0xF3,0xA6,0x74,0x18,0xBE,0x9E,0x7D, + 0xE8,0x5F,0x00,0x33,0xC0,0xCD,0x16,0x5E,0x1F,0x8F,0x04,0x8F,0x44,0x02,0xCD,0x19, + 0x58,0x58,0x58,0xEB,0xE8,0x8B,0x47,0x1A,0x48,0x48,0x8A,0x1E,0x0D,0x7C,0x32,0xFF, + 0xF7,0xE3,0x03,0x06,0x49,0x7C,0x13,0x16,0x4B,0x7C,0xBB,0x00,0x07,0xB9,0x03,0x00, + 0x50,0x52,0x51,0xE8,0x3A,0x00,0x72,0xD8,0xB0,0x01,0xE8,0x54,0x00,0x59,0x5A,0x58, + 0x72,0xBB,0x05,0x01,0x00,0x83,0xD2,0x00,0x03,0x1E,0x0B,0x7C,0xE2,0xE2,0x8A,0x2E, + 0x15,0x7C,0x8A,0x16,0x24,0x7C,0x8B,0x1E,0x49,0x7C,0xA1,0x4B,0x7C,0xEA,0x00,0x00, + 0x70,0x00,0xAC,0x0A,0xC0,0x74,0x29,0xB4,0x0E,0xBB,0x07,0x00,0xCD,0x10,0xEB,0xF2, + 0x3B,0x16,0x18,0x7C,0x73,0x19,0xF7,0x36,0x18,0x7C,0xFE,0xC2,0x88,0x16,0x4F,0x7C, + 0x33,0xD2,0xF7,0x36,0x1A,0x7C,0x88,0x16,0x25,0x7C,0xA3,0x4D,0x7C,0xF8,0xC3,0xF9, + 0xC3,0xB4,0x02,0x8B,0x16,0x4D,0x7C,0xB1,0x06,0xD2,0xE6,0x0A,0x36,0x4F,0x7C,0x8B, + 0xCA,0x86,0xE9,0x8A,0x16,0x24,0x7C,0x8A,0x36,0x25,0x7C,0xCD,0x13,0xC3,0x0D,0x0A, + 0x4E,0x6F,0x6E,0x2D,0x53,0x79,0x73,0x74,0x65,0x6D,0x20,0x64,0x69,0x73,0x6B,0x20, + 0x6F,0x72,0x20,0x64,0x69,0x73,0x6B,0x20,0x65,0x72,0x72,0x6F,0x72,0x0D,0x0A,0x52, + 0x65,0x70,0x6C,0x61,0x63,0x65,0x20,0x61,0x6E,0x64,0x20,0x70,0x72,0x65,0x73,0x73, + 0x20,0x61,0x6E,0x79,0x20,0x6B,0x65,0x79,0x20,0x77,0x68,0x65,0x6E,0x20,0x72,0x65, + 0x61,0x64,0x79,0x0D,0x0A,0x00,0x49,0x4F,0x20,0x20,0x20,0x20,0x20,0x20,0x53,0x59, + 0x53,0x4D,0x53,0x44,0x4F,0x53,0x20,0x20,0x20,0x53,0x59,0x53,0x00,0x00,0x55,0xAA, + ] + \ + [0xF0,0xFF,0xFF] + ([0]*(0x1200-3)) + \ + [0xF0,0xFF,0xFF] + ([0]*(0x1200-3)) + [ + 0x42,0x4C,0x41,0x4E,0x4B,0x20,0x20,0x20,0x20,0x20,0x20,0x28,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0xEE,0x7C,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + ] + ([0] * (0x4200-0x2620)) + \ + ([0xF6] * (0x168000-0x4200)) + + def __init__(self,data=blank_floppy): + """Create Floppy() instance from image bytes, or pre-formatted blank DOS floppy by default.""" + self.data = bytearray(data) + self._boot_open() + self._fat_open() + + def _boot_open(self): + """Parses the boot sector.""" + if (len(self.data) < 38): + raise self.Error("Not enough data in image for boot sector. (%d bytes)" % len(data)) + boot = struct.unpack("= 54: + self.volume_id = struct.unpack(" len(self.data): + raise self.Error("Not enough data to contain %d x %d byte sectors? (%d bytes)" % + (self.sectors, self.sector_size, len(self.data))) + self.root = self.sector_size * (self.reserved_sects + (self.fat_count * self.fat_sects)) + root_sectors = ((self.root_max * 32) + (self.sector_size-1)) // self.sector_size # round up to fill sector + self.cluster2 = self.root + (self.sector_size * root_sectors) + + def _boot_flush(self): + """Commits changes to the boot sector.""" + boot = struct.pack("= 54: + self.data[39:43] = bytearray(struct.pack("> 4) | (self.data[e+1] << 4) + e += 2 + self.fat.append(entry) + + def _fat_flush(self): + """Commits changes to the FAT table.""" + fat_start = self.reserved_sects * self.sector_size + fat_sects = self.fat_sects * self.sector_size + fat_end = fat_start + fat_sects + # build FAT 0 + e = self.reserved_sects * self.sector_size + for i in range(len(self.fat)): + entry = self.fat[i] + if (i & 1) == 0: + self.data[e+0] = entry & 0xFF + self.data[e+1] = (self.data[e+1] & 0xF0) | ((entry >> 8) & 0x0F) + e += 1 + else: + self.data[e+0] = (self.data[e+0] & 0x0F) | ((entry << 4) & 0xF0) + self.data[e+1] = (entry >> 4) & 0xFF + e += 2 + # copy to all tables + for i in range(1,self.fat_count): + fat2_start = fat_start + (fat_sects * i) + fat2_end = fat2_start + fat_sects + self.data[fat2_start:fat2_end] = self.data[fat_start:fat_end] + + def fat_info(self): + """String of information about the FAT.""" + per_line = 32 + s = "" + for i in range(len(self.fat)): + if (i % per_line) == 0: + s += "%03X: " % i + if (i % per_line) == (per_line // 2): + s += " " # extra space to mark 16 + s += " %03X" % self.fat[i] + if (i % per_line) == (per_line - 1): + s += "\n" + return s + + def _cluster_offset(self, cluster): + """Image offset of a FAT indexed logical cluster.""" + if cluster < 2: + return self.root + return self.cluster2 + (self.sector_size * self.cluster_sects * (cluster-2)) + + def _read_chain(self, cluster, size): + data = bytearray() + """Returns up to size bytes from a chain of clusters starting at cluster.""" + while cluster < 0xFF0: + offset = self._cluster_offset(cluster) + #print("read_chain(%03X,%d) at %08X" % (cluster,size,offset)) + if cluster < 2: + return data + bytearray(self.data[self.root:self.root+size]) # root directory + read = min(size,(self.sector_size * self.cluster_sects)) + data = data + bytearray(self.data[offset:offset+read]) + size -= read + if (size < 1): + return data + cluster = self.fat[cluster] + return data + + def _read_dir_chain(self, cluster): + """Reads an entire directory chain starting at the given cluster (0 for root).""" + if cluster < 2: + # root directory is contiguous + return self.data[self.root:self.root+(self.root_max*32)] + # directories just occupy as many clusters as in their FAT chain, using a dummy max size + return self._read_chain(cluster, self.sector_size*self.sectors//self.cluster_sects) + + def _delete_chain(self, cluster): + """Deletes a FAT chain.""" + while cluster < 0xFF0 and cluster >= 2: + link = self.fat[cluster] + self.fat[cluster] = 0 + cluster = link + + def _add_chain(self,data): + """Adds a block of data to the disk and creates its FAT chain. Returns start cluster, or -1 for failure.""" + cluster_size = self.sector_size * self.cluster_sects + clusters = (len(data) + cluster_size-1) // cluster_size + if clusters < 1: + clusters = 1 + # find a chain of free clusters + chain = [] + for i in range(2,len(self.fat)): + if self.fat[i] == 0: + chain.append(i) + if len(chain) >= clusters: + break + if len(chain) < clusters: + return -1 # out of space + # store the FAT chain + start_cluster = chain[0] + for i in range(0,len(chain)-1): + self.fat[chain[i]] = chain[i+1] + self.fat[chain[len(chain)-1]] = 0xFFF + # store the data in the given clusters + data = bytearray(data) + c = 0 + while len(data) > 0: + write = min(len(data),cluster_size) + offset = self._cluster_offset(chain[c]) + self.data[offset:offset+write] = data[0:write] + c += 1 + data = data[write:] + # done + return start_cluster + + def _files_dir(self, cluster, path): + """Returns a list of files in the directory starting at the given cluster. Recursive.""" + #print("files_dir(%03X,'%s')" % (cluster, path)) + entries = [] + directory = self._read_dir_chain(cluster) + for i in range(len(directory) // 32): + e = self.FileEntry(directory[(i*32):(i*32)+32],cluster,i) + #print(("entry %d (%08X)\n"%(i,self._dir_entry_offset(cluster,i)))+e.info()) + if e.incipit == 0x00: # end of directory + return entries + if e.incipit == Floppy.EMPTY: # empty + continue + if (e.attributes & 0x08) != 0: # volume label + continue + if (e.attributes & 0x10) != 0: # subdirectory + if e.path == "." or e.path == "..": + continue + subdir = path + e.path + "\\" + entries.append(subdir) + entries = entries + self._files_dir(e.cluster,subdir) + else: + entries.append(path + e.path) + return entries + + def files(self): + """Returns a list of files in the image.""" + root = self.sector_size * (self.reserved_sects + (self.fat_count * self.fat_sects)) + return self._files_dir(0,"") + + def files_info(self): + """String of the file list.""" + s = "" + for path in self.files(): + s += path + "\n" + return s + + def _dir_entry_offset(self,cluster,dir_index): + """Find the offset in self.data to a particular directory entry.""" + if (cluster < 2): + return self.root + (32 * dir_index) + if (cluster >= 0xFF0): + raise self.Error("Directory entry %d not in its cluster chain?" % dir_index) + per_cluster = (self.sector_size*self.cluster_sects)//32 + if (dir_index < per_cluster): # within this cluster + return self._cluster_offset(cluster) + (32 * dir_index) + # continue to next cluster + return self._dir_entry_offset(self.fat[cluster],dir_index-per_cluster) + + def delete_file(self, entry): + """Deletes a FileEntry.""" + self._delete_chain(entry.cluster) # delete its FAT chain + offset = self._dir_entry_offset(entry.dir_cluster,entry.dir_index) + self.data[offset+0] = Floppy.EMPTY # empty this entry + + def _find_path_dir(self, cluster, path): + """Recursive find path, breaking out subdirectories progressively.""" + #print("_find_path_dir(%03X,'%s')" % (cluster,path)) + separator = path.find("\\") + path_seek = path + path_next = "" + if separator >= 0: + path_seek = path[0:separator] + path_next = path[separator+1:] + directory = self._read_dir_chain(cluster) + for i in range(len(directory) // 32): + e = self.FileEntry(directory[(i*32):(i*32)+32],cluster,i) + if e.incipit == 0x00: # end of directory + return None + if e.incipit == Floppy.EMPTY: # empty + continue + if (e.attributes & 0x08) != 0: # volume label + continue + if (e.attributes & 0x10) != 0: # subdirectory + if e.path == "." or e.path == "..": + continue + if e.path == path_seek: + if (len(path_next) > 0): + return self._find_path_dir(e.cluster, path_next) + else: + return e + elif e.path == path_seek and path_next == "": + return e + return None + + def find_path(self, path): + """Finds a FileEntry for a given path.""" + return self._find_path_dir(0,path) + + def _delete_tree(self,de): + """Recursively deletes directory entries.""" + #print("_delete_tree\n" + de.info()) + directory = self._read_dir_chain(de.cluster) + for i in range(len(directory) // 32): + e = self.FileEntry(directory[(i*32):(i*32)+32],de.cluster,i) + #print(e.info()) + if e.incipit == 0x00: # end of directory + return + if e.incipit == Floppy.EMPTY: # empty + continue + if (e.attributes & 0x08) != 0: # volume label + continue + if (e.attributes & 0x10) != 0: # subdirectory + if e.path == "." or e.path == "..": + continue + self._delete_tree(e) # recurse + self.delete_file(e) + else: + self.delete_file(e) + + def delete_path(self, path): + """Finds a file or directory and deletes it (recursive), if it exists. Returns True if successful.""" + e = self.find_path(path) + if (e == None): + return False + if (e.attributes & 0x10) != 0: + self._delete_tree(e) + self.delete_file(e) + return True + + def _add_entry(self, dir_cluster, entry): + """ + Adds an entry to a directory starting at the given cluster, appending a new cluster if needed. + Sets entry.dir_cluster and entry.dir_index to match their new directory. + Returns False if out of space. + """ + #print(("_add_entry(%d):\n"%dir_cluster) + entry.info()) + directory = self._read_dir_chain(dir_cluster) + dir_len = len(directory)//32 + i = 0 + terminal = False + while i < dir_len: + e = self.FileEntry(directory[(i*32):(i*32)+32],dir_cluster,i) + #print(e.info()) + if e.incipit == 0x00: + terminal = True # make sure to add another terminal entry after this one + break + if e.incipit == Floppy.EMPTY: + break + i += 1 + # extend directory if out of room + if i >= dir_len: + if dir_cluster < 2: + return False # no room in root + # add a zero-filled page to the end of this directory's FAT chain + chain = self._add_chain(bytearray([0]*(self.sector_size*self.cluster_sects))) + if (chain < 0): + return False # no free clusters + tail = dir_cluster + while self.fat[tail] < 0xFF0: + tail = self.fat[tail] + self.fat[tail] = chain + self.fat[chain] = 0xFFF + # insert entry + entry.dir_cluster = dir_cluster + entry.dir_index = i + offset = self._dir_entry_offset(dir_cluster,i) + self.data[offset:offset+32] = entry.compile() + # add a new terminal if needed + if terminal: + i += 1 + if i < dir_len: # if it was the last entry, no new terminal is needed + offset = self._dir_entry_offset(dir_cluster,i) + self.data[offset:offset+32] = Floppy.FileEntry.new_terminal().compile() + + # success! + return True + + def _add_dir_recursive(self, cluster, path): + """Recursively creates directory, returns cluster of created dir, -1 if failed.""" + #print("_add_dir_recursive(%03X,'%s')"%(cluster,path)) + separator = path.find("\\") + path_seek = path + path_next = "" + if separator >= 0: + path_seek = path[0:separator] + path_next = path[separator+1:] + directory = self._read_dir_chain(cluster) + for i in range(len(directory) // 32): + e = self.FileEntry(directory[(i*32):(i*32)+32],cluster,i) + #print(e.info()) + if e.incipit == 0x00: # end of directory + break + if e.incipit == Floppy.EMPTY: # empty + continue + if (e.attributes & 0x10) != 0: # subdirectory + if e.path == path_seek: # already exists + if len(path_next) < 1: + return e.cluster # return existing directory + else: + return self._add_dir_recursive(e.cluster,path_next) # keep descending + # not found: create the directory + dp0 = Floppy.FileEntry.new_dir("_") # . + dp1 = Floppy.FileEntry.new_dir("__") # .. + dp1.cluster = cluster + dir_block = dp0.compile() + dp1.compile() + bytearray([0] * ((self.sector_size*self.cluster_sects)-64)) + new_cluster = self._add_chain(dir_block) + if new_cluster < 0: + return -1 # out of space + # fix up "." to point to itself + dp0.cluster = new_cluster + offset = self._dir_entry_offset(new_cluster,0) + self.data[offset:offset+32] = dp0.compile() + # fix special directory names (FileEntry.compile() is incapable of . or ..) + self.data[offset+ 0:offset+11] = bytearray(". ".encode("ASCII")) + self.data[offset+32:offset+43] = bytearray(".. ".encode("ASCII")) + # create entry to point to new directory cluster + new_dir = Floppy.FileEntry.new_dir(path_seek) + new_dir.cluster = new_cluster + if not self._add_entry(cluster, new_dir): + self._delete_chain(new_cluster) + return -1 # out of space + # return the entry if tail is reached, or keep descending + if len(path_next) < 1: + return new_cluster + else: + return self._add_dir_recursive(new_cluster,path_next) + + def add_dir_path(self, path): + """ + Recursively ensures that the given directory path exists, creating it if necessary. + Path should not end in backslash. + Returns cluster of directory at path, or -1 if failed. + """ + if (len(path) < 1): + return 0 # root + return self._add_dir_recursive(0,path) + + def add_file_path(self, path, data): + """ + Adds the given data as a file at the given path. + Will automatically create directories to complete the path. + Returns False if failed. + """ + self.delete_path(path) # remove file if it already exists + dir_path = "" + file_path = path + separator = path.rfind("\\") + if (separator >= 0): + dir_path = path[0:separator] + file_path = path[separator+1:] + dir_cluster = self.add_dir_path(dir_path) + if dir_cluster < 0: + return False # couldn't find or create directory + cluster = self._add_chain(data) + if (cluster < 0): + return False # out of space + entry = Floppy.FileEntry.new_file(file_path) + entry.cluster = cluster + entry.size = len(data) + if not self._add_entry(dir_cluster, entry): + self._delete_chain(cluster) + return False # out of space for directory entry + offset = self._dir_entry_offset(entry.dir_cluster, entry.dir_index) + self.data[offset:offset+32] = entry.compile() + return True + + def extract_file_path(self, path): + """Finds a file and returns all of its data. None on failure.""" + e = self.find_path(path) + if e == None: + return None + return self._read_chain(e.cluster, e.size) + + def set_volume_id(self, value=None): + """Sets the volume ID. None for time-based.""" + if (value == None): + (date,time) = Floppy.FileEntry.fat_time_now() + value = time | (date << 16) + self.volume_id = value & 0xFFFFFFFF + + def set_volume_label(self, label): + """Sets the volume label. Creates one if necessary.""" + self.data[38] = 0x29 + self.volume_label = label + self.data[54:62] = Floppy._filestring("FAT12",8) + # adjust existing volume entry in root + directory = self._read_dir_chain(0) + for i in range(len(directory) // 32): + e = self.FileEntry(directory[(i*32):(i*32)+32],0,i) + if e.incipit == 0x00: # end of directory + break + if e.incipit == Floppy.EMPTY: # empty + continue + if (e.attributes & 0x08) != 0: # existing volume label + offset = self._dir_entry_offset(0,i) + self.data[offset:offset+11] = Floppy._filestring(label,11) + return + # volume entry does not exist in root, add one + self._add_entry(0,Floppy.FileEntry.new_volume(label)) + return + + def open(filename): + """Opens a floppy image file and creates a Floppy() instance from it.""" + return Floppy(open(filename,"rb").read()) + + def flush(self): + """Commits all unfinished changes to self.data image.""" + self._fat_flush() + self._boot_flush() + + def save(self, filename): + """Saves image (self.data) to file. Implies flush().""" + self.flush() + open(filename,"wb").write(self.data) + + def extract_all(self, out_directory): + """Extracts all files from image to specified directory.""" + for in_path in self.files(): + out_path = os.path.join(out_directory,in_path) + out_dir = os.path.dirname(out_path) + if not os.path.exists(out_dir): + try: + os.makedirs(out_dir) + except OSError as e: + if e.errono != errno.EEXIST: + raise + if not in_path.endswith("\\"): + open(out_path,"wb").write(self.extract_file_path(in_path)) + print(out_path) + else: + print(out_path) + + def add_all(self, in_directory, prefix=""): + """ + Adds all files from specified directory to image. + Files will be uppercased. Long filenames are not checked and will cause an exception. + prefix can be used to prefix a directory path (ending with \) to the added files. + """ + result = True + def dospath(s): + s = s.upper() + s = s.replace("/","\\") + return s + if len(in_directory) < 1: + in_directory = "." + in_directory = os.path.normpath(in_directory) + os.sep + for (root, dirs, files) in os.walk(in_directory): + base = root[len(in_directory):] + for d in dirs: + dir_path = prefix + dospath(os.path.join(base,d)) + result = result and (self.add_dir_path(dir_path) >= 0) + print(dir_path + "\\") + for f in files: + file_path = prefix + dospath(os.path.join(base,f)) + data = open(os.path.join(root,f),"rb").read() + result = result and self.add_file_path(file_path,data) + print(file_path + " (%d bytes)" % len(data)) + return result diff --git a/_tools/lib/tupfile_parser.py b/_tools/lib/tupfile_parser.py new file mode 100644 index 0000000000..2fbf812bc3 --- /dev/null +++ b/_tools/lib/tupfile_parser.py @@ -0,0 +1,153 @@ +# Copyright Magomed Kostoev +# Published under MIT license + +class Rule: + pass + +def get_config(config_name): + if config_name == "KPACK_CMD": + # Just never pack the file for now + return "" + else: + print(f"Unknown config name: {config_name}") + exit(-1) + +def skip_whitespaces(src, ptr): + while len(src) > ptr and src[ptr] == " ": + ptr += 1 + return ptr + +# Returns True if @src has @string starting from @ptr index +def match_string(src, ptr, string): + if len(src) <= ptr + len(string): + return False + for i in range(len(string)): + if src[ptr + i] != string[i]: + return False + return True + +def parse_tup_getconfig(src, ptr): + # Skip get straight to the argument + ptr += len("tup.getconfig(") + ptr = skip_whitespaces(src, ptr) + if src[ptr] != "\"": + print("Expected \"config name\" as tup.getconfig parameter") + exit() + (config_name, ptr) = parse_string(src, ptr) + ptr = skip_whitespaces(src, ptr) + # Skip closing parenthese of the tup.getconfig call + assert(src[ptr] == ")") + ptr += 1 + return (get_config(config_name), ptr) + +def parse_string(src, ptr): + ptr += 1 + string = "" + while src[ptr] != "\"": + string += src[ptr] + ptr += 1 + # Skip the closing "\"" + ptr += 1 + ptr = skip_whitespaces(src, ptr) + # Check if we have concatination here + if match_string(src, ptr, ".."): + # Skip the ".." + ptr += 2 + # The expression parsing should result in a string + (string_to_add, ptr) = parse_expression(src, ptr) + # Concat our string to the resulting string + string += string_to_add + return (string, ptr) + +def parse_expression(src, ptr): + ptr = skip_whitespaces(src, ptr) + result = "WAT?!" + if src[ptr] == "\"": + (result, ptr) = parse_string(src, ptr) + elif match_string(src, ptr, "tup.getconfig("): + (result, ptr) = parse_tup_getconfig(src, ptr) + else: + print(f"Can't handle anything starting with '{src[ptr]}'") + exit(-1) + ptr = skip_whitespaces(src, ptr) + return (result, ptr) + +def expect_comma(src, ptr): + comma_skept = False + ptr = skip_whitespaces(src, ptr) + if src[ptr] == ",": + ptr += 1 + return (True, ptr) + else: + return (False, ptr) + +def parse_arguments(src, ptr): + result = [] + # Parse first argument + (argument, ptr) = parse_expression(src, ptr) + result.append(argument) + (comma_encoutered, ptr) = expect_comma(src, ptr) + # Parse the second argument if it's there + if comma_encoutered: + (argument, ptr) = parse_expression(src, ptr) + result.append(argument) + (comma_encoutered, ptr) = expect_comma(src, ptr) + # Parse third argument if it's there + if comma_encoutered: + (argument, ptr) = parse_expression(src, ptr) + result.append(argument) + return result + +def parse_rule(src, ptr): + # Get straight to the first argument + ptr += len("tup.rule(") + # Parse the arguments + args = parse_arguments(src, ptr) + # Build the rule object + result = Rule() + if len(args) == 3: + result.input = args[0] + result.command = args[1] + result.output = args[2] + # Replace %f with input file in rule's command + if type(result.input == str): + result.command = result.command.replace("%f", result.input) + else: + print("Command building with non-string tup.rule's first argument" + + " isn't implemented") + exit() + # Replace %o with output file in rule's command + if type(result.output == str): + result.command = result.command.replace("%o", result.output) + else: + print("Command building with non-string tup.rule's first argument" + + " isn't implemented") + exit() + elif len(args) == 2: + result.input = [] + result.command = args[0] + result.output = args[1] + else: + print(f"tup.rule can only take 2 or 3 arguments, not {len(args)}") + exit(-1) + # Unify the API - return arrays as input and output + if type(result.input) == str: + result.input = [ result.input ] + else: + assert(type(result.input) == list) + if type(result.output) == str: + result.output = [ result.output ] + else: + assert(type(result.output) == list) + return result + +def parse(file_name): + rules = [] + with open(file_name) as f: + tupfile = f.read() + rule_begin_index = tupfile.find("tup.rule(") + while (rule_begin_index != -1): + rules.append(parse_rule(tupfile, rule_begin_index)) + # Find the next tup.rule call + rule_begin_index = tupfile.find("tup.rule(", rule_begin_index + len("tup.rule(")) + return rules diff --git a/_tools/workspace/build.py b/_tools/workspace/build.py new file mode 100644 index 0000000000..06fc7dfbc4 --- /dev/null +++ b/_tools/workspace/build.py @@ -0,0 +1,24 @@ +import sys +import os + +path_to_lib = '../lib' +sys.path.append(path_to_lib) + +import tupfile_parser + +def build(): + if not os.path.exists("Tupfile.lua"): + print("No Tupfile.lua, can't build anything") + exit() + + tup_rules = tupfile_parser.parse("Tupfile.lua") + program_files = [] + for rule in tup_rules: + # TODO: Manage source dependencies + # TODO: Inform about tools required for the build + os.system(rule.command) + program_files += rule.output + return program_files + +if __name__ == "__main__": + build() \ No newline at end of file diff --git a/_tools/workspace/run.py b/_tools/workspace/run.py new file mode 100644 index 0000000000..3c6a3b7f12 --- /dev/null +++ b/_tools/workspace/run.py @@ -0,0 +1,103 @@ +import os +import sys +import shutil +import urllib.request +import subprocess + +import build + +path_to_lib = '../lib' +sys.path.append(path_to_lib) + +from makeflop import Floppy + +# TODO: Move into _tools/lib +def get_file_directory(path): + path = path.replace("\\", "/") + if "/" in path: + folder = "/".join(path.split("/")[:-1]) + if folder == "": + return "/" # It was a file in the root folder + return folder + else: + return "." # Just a filename, let's return current folder + +# TODO: Move into _tools/lib +def is_win32(): + return True if sys.platform == "win32" else False + +# TODO: Move into _tools/lib +def is_linux(): + return True if sys.platform == "linux" or sys.platform == "linux2" else False + +# TODO: Move into _tools/lib +def is_osx(): + return True if sys.platform == "darwin" else False + +# TODO: Move into _tools/lib +def log(s, end = "\n"): + print(s, end = end, flush = True) + +# TODO: Move into _tools/lib +def download(link, path): + log(f"Downloading {path}... ", end = "") + urllib.request.urlretrieve(link, path) + log("Done.") + +# TODO: Move into _tools/lib +def path(*args): + return os.sep.join(args) + +# TODO: Move into _tools/lib +def run_qemu(start_dir = "workspace"): + qemu_command = f"qemu-system-i386" + flags = "" + flags += "-L . " # IDK why it does not work without this + flags += "-m 128 " + flags += f"-drive format=raw,file={start_dir}/kolibri.img,index=0,if=floppy -boot a " + flags += "-vga vmware " + flags += "-net nic,model=rtl8139 -net user " + flags += "-soundhw ac97 " + if is_win32(): + qemu_full_path = shutil.which(qemu_command) + qemu_directory = get_file_directory(qemu_full_path) + flags += f"-L {qemu_directory} " + s = f"{qemu_command} {flags}" + qemu_stdout = open(f"{start_dir}/qemu_stdout.log", "w") + qemu_stderr = open(f"{start_dir}/qemu_stderr.log", "w") + if is_win32(): + return subprocess.Popen(s, bufsize = 0, stdout = qemu_stdout, stderr = qemu_stderr, stdin = subprocess.DEVNULL, shell = True, start_new_session = True) + else: + a = shlex.split(s) + return subprocess.Popen(a, bufsize = 0, stdout = qemu_stdout, stderr = qemu_stderr, stdin = subprocess.DEVNULL, start_new_session = True) + +if __name__ == "__main__": + program_files = build.build() + + os.makedirs("workspace", exist_ok = True) + + if not os.path.exists("workspace/kolibri.img"): + img_url = "http://builds.kolibrios.org/eng/data/data/kolibri.img" + download(img_url, "workspace/kolibri.img") + + # Open the IMG + with open("workspace/kolibri.img", "rb") as img: + img_data = img.read() + img = Floppy(img_data) + + # Remove unuseful folders + img.delete_path("GAMES") + img.delete_path("DEMOS") + img.delete_path("3D") + + log("Moving program files into kolibri image... ", end = "") + for file_name in program_files: + with open(file_name, "rb") as file: + file_data = file.read() + if not img.add_file_path(file_name, file_data): + print(f"Coudn't move {file_name} into IMG") + img.save("workspace/kolibri.img") + log("Done") + + # TODO: Autorun + run_qemu()