From 31afceeade6dd852a8b7a71805b21a20e502229a Mon Sep 17 00:00:00 2001
From: dec05eba <dec05eba@protonmail.com>
Date: Tue, 5 Jun 2018 14:28:40 +0200
Subject: Add sibs init to init project in existing directory

---
 include/FileUtil.hpp |   3 +-
 src/Conf.cpp         |   4 +
 src/FileUtil.cpp     |  12 +++
 src/main.cpp         | 260 ++++++++++++++++++++++++++++++++++++++++++++-------
 4 files changed, 242 insertions(+), 37 deletions(-)

diff --git a/include/FileUtil.hpp b/include/FileUtil.hpp
index 7288426..b808573 100644
--- a/include/FileUtil.hpp
+++ b/include/FileUtil.hpp
@@ -21,7 +21,7 @@ namespace sibs
 	using FileString = std::basic_string<_tinydir_char_t, std::char_traits<_tinydir_char_t>, std::allocator<_tinydir_char_t>>;
 
 #if OS_FAMILY == OS_FAMILY_POSIX
-#define toUtf8(input) input
+    #define toUtf8(input) input
     FileString toFileString(const std::string &utf8Str);
     FileString toFileString(const StringView &utf8Str);
 #else
@@ -49,6 +49,7 @@ namespace sibs
     void walkDirFiles(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc);
     void walkDirFilesRecursive(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc);
     Result<StringView> getFileContent(const _tinydir_char_t *filepath);
+    Result<bool> fileWrite(const _tinydir_char_t *filepath, StringView data);
     Result<bool> fileOverwrite(const _tinydir_char_t *filepath, StringView data);
     Result<FileString> getHomeDir();
     Result<FileString> getCwd();
diff --git a/src/Conf.cpp b/src/Conf.cpp
index 883022b..4424551 100644
--- a/src/Conf.cpp
+++ b/src/Conf.cpp
@@ -570,12 +570,16 @@ namespace sibs
     
     bool isProjectNameValid(const string &projectName)
     {
+        if(projectName.empty())
+            return false;
+        
         for(int i = 0; i < projectName.size(); ++i)
         {
             char c = projectName[i];
             if(!isalpha(c) && !isdigit(c) && c != '-' && c != '_')
                 return false;
         }
+        
         return true;
     }
     
diff --git a/src/FileUtil.cpp b/src/FileUtil.cpp
index 53c1132..aca2778 100644
--- a/src/FileUtil.cpp
+++ b/src/FileUtil.cpp
@@ -229,6 +229,18 @@ namespace sibs
         fclose(file);
         return Result<StringView>::Ok(StringView(result, fileSize));
     }
+    
+    Result<bool> fileWrite(const _tinydir_char_t *filepath, StringView data)
+    {
+        if(getFileType(filepath) != FileType::FILE_NOT_FOUND)
+        {
+            string errMsg = "Failed to write to file: ";
+            errMsg += toUtf8(filepath);
+            errMsg += "; reason: file already exists";
+            return Result<bool>::Err(errMsg);
+        }
+        return fileOverwrite(filepath, data);
+    }
 
     Result<bool> fileOverwrite(const _tinydir_char_t *filepath, StringView data)
     {
diff --git a/src/main.cpp b/src/main.cpp
index a3f09a8..aabf83e 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -90,8 +90,10 @@ using namespace std::chrono;
 // TODO: Add program command for generating compile_commands.json without compiling code, without using Ninja
 
 #if OS_FAMILY == OS_FAMILY_POSIX
+#define fout std::cout
 #define ferr std::cerr
 #else
+#define fout std::wcout
 #define ferr std::wcerr
 #endif
 
@@ -111,7 +113,7 @@ void usageBuild()
     printf("Usage: sibs build [project_path] [--debug|--release] [--sanitize]\n\n");
     printf("Build a sibs project\n\n");
     printf("Options:\n");
-    printf("  project_path\t\tThe directory containing a project.conf file - Optional (default: current working directory)\n");
+    printf("  project_path\t\tThe directory containing a project.conf file - Optional (default: current directory)\n");
     printf("  --debug|--release\t\tOptimization level to build project and dependencies with (if not a system package) - Optional (default: --debug)\n");
     printf("  --sanitize\t\tAdd runtime address/undefined behavior sanitization. Program can be up to 3 times slower and use 10 times as much RAM. Ignored if compiler doesn't support sanitization - Optional (default: disabled)\n");
     printf("Examples:\n");
@@ -142,7 +144,7 @@ void usageTest()
     printf("Usage: sibs test [project_path] [--no-sanitize]\n\n");
     printf("Build and run tests for a sibs project\n\n");
     printf("Options:\n");
-    printf("  project_path\t\tThe directory containing a project.conf file - Optional (default: current working directory)\n");
+    printf("  project_path\t\tThe directory containing a project.conf file - Optional (default: current directory)\n");
     printf("  --no-sanitize\t\tDisable runtime address/undefined behavior - Optional\n");
     printf("Examples:\n");
     printf("  sibs test\n");
@@ -150,6 +152,21 @@ void usageTest()
     exit(1);
 }
 
+void usageInit()
+{
+    printf("Usage: sibs init [project_path] <--exec|--static|--dynamic>\n\n");
+    printf("Create sibs project structure in an existing directory\n\n");
+    printf("Options:\n");
+    printf("  project_path\t\tThe directory where you want to initialize sibs project - Optional (default: current  directory)\n");
+    printf("  --exec\t\t\tProject compiles to an executable\n");
+    printf("  --static\t\t\tProject compiles to a static library\n");
+    printf("  --dynamic\t\t\tProject compiles to a dynamic library\n");
+    printf("Examples:\n");
+    printf("  sibs init --exec\n");
+    printf("  sibs init dirA/dirB --dynamic");
+    exit(1);
+}
+
 void validateDirectoryPath(const _tinydir_char_t *projectPath)
 {
     FileType projectPathFileType = getFileType(projectPath);
@@ -369,7 +386,7 @@ int buildProject(int argc, const _tinydir_char_t **argv)
 
 int testProject(int argc, const _tinydir_char_t **argv)
 {
-    if(argc > 1)
+    if(argc > 2)
         usageTest();
     
     FileString projectPath;
@@ -431,37 +448,18 @@ int testProject(int argc, const _tinydir_char_t **argv)
     return buildProject(projectPath, projectConfFilePath, sibsConfig);
 }
 
-void newProjectCreateMainDir(const FileString &projectPath)
+// Returns nullptr if @charToFind is not found
+const _tinydir_char_t* findLastOf(const _tinydir_char_t *str, const int strSize, const char charToFind)
 {
-    Result<bool> createProjectDirResult = createDirectoryRecursive(projectPath.c_str());
-    if(createProjectDirResult.isErr())
+    for(int i = strSize; i >= 0; --i)
     {
-        ferr << "Failed to create project main directory: " << toFileString(createProjectDirResult.getErrMsg()) << endl;
-        exit(20);
+        if(str[i] == charToFind)
+            return str + i;
     }
+    return nullptr;
 }
 
-void createProjectSubDir(const FileString &dir)
-{
-    Result<bool> createProjectDirResult = createDirectoryRecursive(dir.c_str());
-    if(createProjectDirResult.isErr())
-    {
-        ferr << "Failed to create directory in project: " << toFileString(createProjectDirResult.getErrMsg()) << endl;
-        exit(20);
-    }
-}
-
-void createProjectFile(const FileString &projectFilePath, const string &fileContent)
-{
-    Result<bool> fileOverwriteResult = fileOverwrite(projectFilePath.c_str(), fileContent.c_str());
-    if(fileOverwriteResult.isErr())
-    {
-        ferr << "Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl;
-        exit(20);
-    }
-}
-
-void newProjectCreateConf(const string &projectName, const string &projectType, const FileString &projectPath)
+Result<bool> newProjectCreateConf(const string &projectName, const string &projectType, const FileString &projectPath)
 {
     string projectConfStr = "[package]\n";
     projectConfStr += "name = \"" + projectName + "\"\n";
@@ -476,10 +474,20 @@ void newProjectCreateConf(const string &projectName, const string &projectType,
     
     FileString projectConfPath = projectPath;
     projectConfPath += TINYDIR_STRING("/project.conf");
-    Result<bool> fileOverwriteResult = fileOverwrite(projectConfPath.c_str(), projectConfStr.c_str());
+    return fileWrite(projectConfPath.c_str(), projectConfStr.c_str());
+}
+
+Result<bool> createDirectoryRecursive(const FileString &dir)
+{
+    return createDirectoryRecursive(dir.c_str());
+}
+
+void createProjectFile(const FileString &projectFilePath, const string &fileContent)
+{
+    Result<bool> fileOverwriteResult = fileOverwrite(projectFilePath.c_str(), fileContent.c_str());
     if(fileOverwriteResult.isErr())
     {
-        ferr << "Failed to create project.conf: " << toFileString(fileOverwriteResult.getErrMsg()) << endl;
+        ferr << "Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl;
         exit(20);
     }
 }
@@ -494,6 +502,169 @@ Result<ExecResult> gitInitProject(const FileString &projectPath)
     return exec(cmd.c_str());
 }
 
+int initProject(int argc, const _tinydir_char_t **argv)
+{
+    if(argc > 2)
+        usageInit();
+    
+    FileString projectPath;
+    const _tinydir_char_t *projectType = nullptr;
+
+    for(int i = 0; i < argc; ++i)
+    {
+        const _tinydir_char_t *arg = argv[i];
+        if(_tinydir_strcmp(arg, TINYDIR_STRING("--exec")) == 0)
+        {
+            if(projectType)
+            {
+                ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl;
+                usageInit();
+            }
+            projectType = arg;
+        }
+        else if(_tinydir_strcmp(arg, TINYDIR_STRING("--static")) == 0)
+        {
+            if(projectType)
+            {
+                ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl;
+                usageInit();
+            }
+            projectType = arg;
+        }
+        else if(_tinydir_strcmp(arg, TINYDIR_STRING("--dynamic")) == 0)
+        {
+            if(projectType)
+            {
+                ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl;
+                usageInit();
+            }
+            projectType = arg;
+        }
+        else if(_tinydir_strncmp(arg, TINYDIR_STRING("--"), 2) == 0)
+        {
+            ferr << "Error: Invalid argument " << arg << endl;
+            usageInit();
+        }
+        else
+        {
+            if(!projectPath.empty())
+            {
+                ferr << "Error: Project path was defined more than once. First defined as " << projectPath << " then as " << arg << endl;
+                usageInit();
+            }
+            projectPath = arg;
+        }
+    }
+    
+    if(!projectType)
+    {
+        ferr << "Error: Project type not defined, expected to be either --exec, --static or --dynamic" << endl;
+        usageInit();
+    }
+    
+    string projectTypeConf;
+    if(_tinydir_strcmp(projectType, TINYDIR_STRING("--exec")) == 0)
+        projectTypeConf = "executable";
+    else if(_tinydir_strcmp(projectType, TINYDIR_STRING("--static")) == 0)
+        projectTypeConf = "static";
+    else if(_tinydir_strcmp(projectType, TINYDIR_STRING("--dynamic")) == 0)
+        projectTypeConf = "dynamic";
+    else
+    {
+        ferr << "Expected project type to be either --exec, --static or --dynamic; was: " << projectType << endl << endl;
+        usageInit();
+    }
+    
+    // TODO: If projectPath is not defined and working directory does not contain project.conf, then search every parent directory until one is found
+    if(projectPath.empty())
+        projectPath = TINYDIR_STRING(".");
+    
+    validateDirectoryPath(projectPath.c_str());
+    if(projectPath.back() != '/')
+        projectPath += TINYDIR_STRING("/");
+
+    Result<FileString> projectRealPathResult = getRealPath(projectPath.c_str());
+    if(!projectRealPathResult)
+    {
+        ferr << "Failed to get real path for: '" << projectPath.c_str() << "': " << toFileString(projectRealPathResult.getErrMsg()) << endl;
+        exit(40);
+    }
+    projectPath = projectRealPathResult.unwrap();
+    
+    FileType projectFileType = getFileType(projectPath.c_str());
+    if(projectFileType == FileType::FILE_NOT_FOUND)
+    {
+        ferr << "Directory not found: '" << projectPath << "', unable to initialize project" << endl;
+        exit(20);
+    }
+    else if(projectFileType == FileType::REGULAR)
+    {
+        ferr << "Expected project path : '" << projectPath << "' to be a directory, was a file. Unable to initialize project" << endl;
+        exit(21);
+    }
+    
+    const _tinydir_char_t *projectNameForwardSlash = findLastOf(projectPath.c_str(), projectPath.size(), '/');
+    const _tinydir_char_t *projectNameBackwardSlash = findLastOf(projectPath.c_str(), projectPath.size(), '\\');
+    string projectName;
+    if(projectNameForwardSlash && projectNameBackwardSlash)
+    {
+        if(projectNameForwardSlash > projectNameBackwardSlash)
+            projectName = toUtf8(projectNameForwardSlash + 1);
+        else
+            projectName = toUtf8(projectNameBackwardSlash + 1);
+    }
+    else if(!projectNameForwardSlash && projectNameBackwardSlash)
+        projectName = toUtf8(projectNameBackwardSlash + 1);
+    else if(!projectNameBackwardSlash && projectNameForwardSlash)
+        projectName = toUtf8(projectNameForwardSlash + 1);
+    else
+        projectName = toUtf8(projectPath);
+
+    if(!isProjectNameValid(projectName))
+    {
+        ferr << "Project name can only contain alphanumerical characters, dash (-) or underscore (_) and has to be longer than 0 characters" << endl;
+        exit(20);
+    }
+    
+    auto createProjectConfResult = newProjectCreateConf(projectName, projectTypeConf, projectPath);
+    if(!createProjectConfResult)
+    {
+        ferr << "A project already exists in the directory " << projectPath << ". Error: failed to create project.conf, reason: " << createProjectConfResult.getErrMsg() << endl;
+        exit(20);
+    }
+    createDirectoryRecursive(projectPath + TINYDIR_STRING("/src"));
+    createDirectoryRecursive(projectPath + TINYDIR_STRING("/include"));
+    createDirectoryRecursive(projectPath + TINYDIR_STRING("/tests"));
+    if(projectTypeConf == "executable")
+    {
+        auto mainFilePath = projectPath + TINYDIR_STRING("/src/main.cpp");
+        Result<bool> fileOverwriteResult = fileWrite(mainFilePath.c_str(), "#include <cstdio>\n\nint main(int argc, char **argv)\n{\n    printf(\"hello, world!\\n\");\n    return 0;\n}\n");
+        if(!fileOverwriteResult)
+            fout << "Warning: Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl;
+    }
+    gitInitProject(projectPath);
+    return 0;
+}
+
+void newProjectCreateMainDir(const FileString &projectPath)
+{
+    Result<bool> createProjectDirResult = createDirectoryRecursive(projectPath.c_str());
+    if(createProjectDirResult.isErr())
+    {
+        ferr << "Failed to create project main directory: " << toFileString(createProjectDirResult.getErrMsg()) << endl;
+        exit(20);
+    }
+}
+
+void checkFailCreateSubDir(Result<bool> createSubDirResult)
+{
+    if(!createSubDirResult)
+    {
+        ferr << "Failed to create directory in project: " << toFileString(createSubDirResult.getErrMsg()) << endl;
+        exit(20);
+    }
+}
+
 int newProject(int argc, const _tinydir_char_t **argv)
 {
     if(argc != 2)
@@ -542,12 +713,25 @@ int newProject(int argc, const _tinydir_char_t **argv)
     }
     
     newProjectCreateMainDir(projectPath);
-    newProjectCreateConf(projectName, projectTypeConf, projectPath);
-    createProjectSubDir(projectPath + TINYDIR_STRING("/src"));
-    createProjectSubDir(projectPath + TINYDIR_STRING("/include"));
-    createProjectSubDir(projectPath + TINYDIR_STRING("/tests"));
+    auto createProjectConfResult = newProjectCreateConf(projectName, projectTypeConf, projectPath);
+    if(!createProjectConfResult)
+    {
+        ferr << "Failed to create project.conf: " << toFileString(createProjectConfResult.getErrMsg()) << endl;
+        exit(20);
+    }
+    createDirectoryRecursive(projectPath + TINYDIR_STRING("/src"));
+    createDirectoryRecursive(projectPath + TINYDIR_STRING("/include"));
+    createDirectoryRecursive(projectPath + TINYDIR_STRING("/tests"));
     if(projectTypeConf == "executable")
-        createProjectFile(projectPath + TINYDIR_STRING("/src/main.cpp"), "#include <cstdio>\n\nint main(int argc, char **argv)\n{\n    printf(\"hello, world!\\n\");\n    return 0;\n}\n");
+    {
+        auto mainFilePath = projectPath + TINYDIR_STRING("/src/main.cpp");
+        Result<bool> fileOverwriteResult = fileWrite(mainFilePath.c_str(), "#include <cstdio>\n\nint main(int argc, char **argv)\n{\n    printf(\"hello, world!\\n\");\n    return 0;\n}\n");
+        if(!fileOverwriteResult)
+        {
+            ferr << "Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl;
+            exit(20);
+        }
+    }
     // We are ignoring git init result on purpose. If it fails, just ignore it; not important
     gitInitProject(projectPath);
     return 0;
@@ -579,6 +763,10 @@ int wmain(int argc, const _tinydir_char_t **argv)
         {
             return testProject(subCommandArgCount, subCommandArgPtr);
         }
+        else if(_tinydir_strcmp(arg, TINYDIR_STRING("init")) == 0)
+        {
+            return initProject(subCommandArgCount, subCommandArgPtr);
+        }
         else
         {
             ferr << "Expected command to be either 'build', 'new' or 'test', was: " << arg << endl << endl;
-- 
cgit v1.2.3-70-g09d2