Commit 9b1a2112 authored by jan.koester's avatar jan.koester
Browse files

install gnu compatibel

parent ee7a8b18
Loading
Loading
Loading
Loading
+220 −114
Changes for commands/filesystem/install.cpp: 220 added lines, 114 removed lines.
Original line number Diff line number Diff line
@@ -28,116 +28,168 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <cstring>
#include <cerrno>
#include <cstdlib>

#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <utime.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>

#include "shell.h"
#include "modeutil.h"
#include "../text/textutil.h"

namespace fresh { namespace cmd {

    // "install [-m MODE] [-p] [-d] SRC... DST" / "install -d DIR...": the
    // BSD install(1) core used throughout %install sections -- copies each
    // SRC to DST (a directory, or the exact target file name for a single
    // SRC), creating any missing leading directories in DST (real install(1)
    // only does this under "-D"; every call here already behaves as if -D
    // were always on, so that flag is accepted but otherwise a no-op), then
    // chmod's the result to MODE (default 0755). "-p" additionally copies
    // SRC's mtime/atime onto DST after that (real install(1) default:
    // fresh timestamps) -- needed for upstream autotools-generated
    // "$(INSTALL) -c -p ..." install rules. "-d" instead creates each
    // argument as a directory (mkdir -p). "-o OWNER"/"-g GROUP" are
    // accepted (consuming their argument) but not applied -- every %install
    // here already runs as root installing to a throwaway buildroot, so
    // ownership beyond that has never mattered; "-c"/"-s"/"-b"/"-v" are
    // accepted as no-ops (traditional compatibility/strip/backup/verbose
    // flags real install(1) also supports).
    // install [-cCDdpsv] [-m MODE] [-o OWNER] [-g GROUP] [-t DIR] [-T] SRC... DST
    // install -d [-m MODE] DIR...
    // GNU semantics: files default to 0755, the destination is replaced
    // (unlinked, then written fresh), leading directories are only created
    // with -D, -s runs $STRIPPROG (default "strip") on the copy.
    class InstallCommand : public ShellCommand {
    public:
        InstallCommand():ShellCommand("install"){}

        static bool mkdirsFor(const std::string &path,std::string &err){
            std::string dir=path;
            size_t slash=dir.find_last_of('/');
            if(slash==std::string::npos)
                return true;
            dir=dir.substr(0,slash);
            if(dir.empty())
                return true;
        bool verbose=false,preserve=false,strip=false,compare=false;
        bool haveOwner=false,haveGroup=false;
        uid_t owner=(uid_t)-1;
        gid_t group=(gid_t)-1;
        mode_t mode=0755;
        std::string stripProg;

            std::string partial;
            size_t start=0;
            if(dir[0]=='/'){ partial="/"; start=1; }
            while(start<=dir.size()){
                size_t next=dir.find('/',start);
                std::string comp=(next==std::string::npos) ? dir.substr(start) : dir.substr(start,next-start);
                if(!comp.empty()){
                    if(!partial.empty() && partial.back()!='/') partial+="/";
                    partial+=comp;
                    if(mkdir(partial.c_str(),0755)!=0 && errno!=EEXIST){
                        err=partial+": "+strerror(errno);
        bool mkdirs(const std::string &dir,mode_t last,bool applyLast){
            std::string cur;
            size_t p=0;
            if(!dir.empty() && dir[0]=='/'){ cur="/"; p=1; }
            while(p<=dir.size()){
                size_t sl=dir.find('/',p);
                std::string comp=dir.substr(p,sl==std::string::npos ? std::string::npos : sl-p);
                p=sl==std::string::npos ? dir.size()+1 : sl+1;
                if(comp.empty()) continue;
                if(!cur.empty() && cur.back()!='/') cur+='/';
                cur+=comp;
                bool isLast=p>dir.size() || dir.find_first_not_of('/',p)==std::string::npos;
                struct stat st;
                if(stat(cur.c_str(),&st)==0){
                    if(!S_ISDIR(st.st_mode)){
                        std::cerr << "install: cannot create directory '" << cur << "': File exists" << std::endl;
                        return false;
                    }
                }else{
                    if(mkdir(cur.c_str(),0755)!=0 && errno!=EEXIST){
                        std::cerr << "install: cannot create directory '" << cur << "': " << strerror(errno) << std::endl;
                        return false;
                    }
                    if(verbose) std::cout << "install: creating directory '" << cur << "'" << std::endl;
                }
                if(isLast && applyLast){
                    if(chmod(cur.c_str(),last)!=0){
                        std::cerr << "install: cannot change permissions of '" << cur << "': " << strerror(errno) << std::endl;
                        return false;
                    }
                    if((haveOwner || haveGroup) && chown(cur.c_str(),owner,group)!=0){
                        std::cerr << "install: cannot change ownership of '" << cur << "': " << strerror(errno) << std::endl;
                        return false;
                    }
                }
                if(next==std::string::npos) break;
                start=next+1;
            }
            return true;
        }

        static bool makeDir(const std::string &path){
            std::string err;
            if(!mkdirsFor(path+"/x",err)){
                std::cerr << "install: " << err << std::endl;
        static bool sameContent(const std::string &a,const std::string &b){
            std::string x,y;
            if(!text::readInput(a,x) || !text::readInput(b,y)) return false;
            return x==y;
        }

        bool installFile(const std::string &src,const std::string &dst){
            struct stat ss;
            if(stat(src.c_str(),&ss)!=0){
                std::cerr << "install: cannot stat '" << src << "': " << strerror(errno) << std::endl;
                return false;
            }
            if(S_ISDIR(ss.st_mode)){
                std::cerr << "install: omitting directory '" << src << "'" << std::endl;
                return false;
            }
            if(mkdir(path.c_str(),0755)!=0 && errno!=EEXIST){
                std::cerr << "install: " << path << ": " << strerror(errno) << std::endl;
            struct stat ds;
            if(stat(dst.c_str(),&ds)==0 && S_ISDIR(ds.st_mode)){
                std::cerr << "install: cannot overwrite directory '" << dst << "' with non-directory '" << src << "'" << std::endl;
                return false;
            }
            if(stat(dst.c_str(),&ds)==0 && ds.st_dev==ss.st_dev && ds.st_ino==ss.st_ino){
                std::cerr << "install: '" << src << "' and '" << dst << "' are the same file" << std::endl;
                return false;
            }
            if(compare && stat(dst.c_str(),&ds)==0 && (ds.st_mode&07777)==mode && S_ISREG(ds.st_mode) &&
               (!haveOwner || ds.st_uid==owner) && (!haveGroup || ds.st_gid==group) && sameContent(src,dst))
                return true;
            int in=open(src.c_str(),O_RDONLY);
            if(in<0){
                std::cerr << "install: cannot open '" << src << "' for reading: " << strerror(errno) << std::endl;
                return false;
            }

        static bool copyFile(const std::string &src,const std::string &dst,mode_t mode,bool preserveTimes){
            std::string err;
            if(!mkdirsFor(dst,err)){
                std::cerr << "install: " << err << std::endl;
            if(unlink(dst.c_str())!=0 && errno!=ENOENT){
                std::cerr << "install: cannot remove '" << dst << "': " << strerror(errno) << std::endl;
                close(in);
                return false;
            }
            std::ifstream in(src.c_str(),std::ios::binary);
            if(!in.is_open()){
                std::cerr << "install: " << src << ": " << strerror(errno) << std::endl;
            int out=open(dst.c_str(),O_WRONLY|O_CREAT|O_EXCL,0600);
            if(out<0){
                std::cerr << "install: cannot create regular file '" << dst << "': " << strerror(errno) << std::endl;
                close(in);
                return false;
            }
            std::ofstream out(dst.c_str(),std::ios::binary|std::ios::trunc);
            if(!out.is_open()){
                std::cerr << "install: " << dst << ": " << strerror(errno) << std::endl;
            char buf[65536];
            bool ok=true;
            for(;;){
                ssize_t r=read(in,buf,sizeof buf);
                if(r<0){ if(errno==EINTR) continue; ok=false; break; }
                if(r==0) break;
                ssize_t off=0;
                while(off<r){
                    ssize_t w=write(out,buf+off,(size_t)(r-off));
                    if(w<0){ if(errno==EINTR) continue; ok=false; break; }
                    off+=w;
                }
                if(!ok) break;
            }
            close(in);
            if(close(out)!=0) ok=false;
            if(!ok){
                std::cerr << "install: error copying '" << src << "' to '" << dst << "': " << strerror(errno) << std::endl;
                return false;
            }
            out << in.rdbuf();
            in.close(); out.close();
            if(chmod(dst.c_str(),mode)!=0){
                std::cerr << "install: " << dst << ": " << strerror(errno) << std::endl;
            if(verbose) std::cout << "'" << src << "' -> '" << dst << "'" << std::endl;
            if(strip){
                pid_t pid=fork();
                if(pid==0){
                    execlp(stripProg.c_str(),stripProg.c_str(),dst.c_str(),(char*)nullptr);
                    std::cerr << "install: cannot run '" << stripProg << "': " << strerror(errno) << std::endl;
                    _exit(127);
                }
                int st=0;
                if(pid<0 || waitpid(pid,&st,0)<0 || !WIFEXITED(st) || WEXITSTATUS(st)!=0){
                    std::cerr << "install: strip process terminated abnormally" << std::endl;
                    unlink(dst.c_str());
                    return false;
                }
            if(preserveTimes){
                struct stat st;
                if(stat(src.c_str(),&st)==0){
                    struct utimbuf times;
                    times.actime=st.st_atime;
                    times.modtime=st.st_mtime;
                    // Not fatal on its own -- a copy that succeeded but
                    // couldn't have its timestamp reset is still a correct
                    // install, just without the one cosmetic extra -p asked
                    // for.
                    if(utime(dst.c_str(),&times)!=0)
                        std::cerr << "install: " << dst << ": " << strerror(errno) << std::endl;
            }
            if((haveOwner || haveGroup) && chown(dst.c_str(),owner,group)!=0){
                std::cerr << "install: cannot change ownership of '" << dst << "': " << strerror(errno) << std::endl;
                return false;
            }
            if(chmod(dst.c_str(),mode)!=0){
                std::cerr << "install: cannot change permissions of '" << dst << "': " << strerror(errno) << std::endl;
                return false;
            }
            if(preserve){
                struct timespec ts[2]={ss.st_atim,ss.st_mtim};
                utimensat(AT_FDCWD,dst.c_str(),ts,0);
            }
            return true;
        }
@@ -146,61 +198,115 @@ namespace fresh { namespace cmd {
            struct stat st;
            return stat(path.c_str(),&st)==0 && S_ISDIR(st.st_mode);
        }

        static std::string baseName(const std::string &path){
            size_t slash=path.find_last_of('/');
            return (slash==std::string::npos) ? path : path.substr(slash+1);
        static std::string baseName(std::string p){
            while(p.size()>1 && p.back()=='/') p.pop_back();
            size_t sl=p.find_last_of('/');
            return sl==std::string::npos ? p : p.substr(sl+1);
        }
        static std::string dirName(std::string p){
            while(p.size()>1 && p.back()=='/') p.pop_back();
            size_t sl=p.find_last_of('/');
            if(sl==std::string::npos) return "";
            return sl==0 ? "/" : p.substr(0,sl);
        }

        int runCommand(int argc,const char **argv){
            bool makeDirs=false;
            bool preserveTimes=false;
            mode_t mode=0755;
            std::vector<std::string> operands;

            for(int i=1;i<argc;++i){
                std::string a=argv[i];
                if(a=="-d") makeDirs=true;
                else if(a=="-m" && i+1<argc) mode=(mode_t)strtol(argv[++i],nullptr,8);
                else if(a=="-p") preserveTimes=true;
                else if(a=="-D") /* every install here already implies -D */;
                else if((a=="-o" || a=="-g") && i+1<argc) ++i;
                else if(a=="-c" || a=="-s" || a=="-b" || a=="-v") /* accepted no-op */;
                else operands.push_back(a);
            verbose=preserve=strip=compare=haveOwner=haveGroup=false;
            owner=(uid_t)-1; group=(gid_t)-1;
            const char *sp=getenv("STRIPPROG");
            stripProg=sp && *sp ? sp : "strip";
            bool dirMode=false,leading=false,noTargetDir=false;
            std::string modeSpec,targetDir,ownerName,groupName;
            text::Opts o=text::parseOpts(argc,argv,"mogtSZ",[&](char c,const std::string &v)->bool{
                switch(c){
                    case 'b': case 'c': case 'Z': return true;
                    case 'S': return true;
                    case 'C': compare=true; return true;
                    case 'd': dirMode=true; return true;
                    case 'D': leading=true; return true;
                    case 'p': preserve=true; return true;
                    case 's': strip=true; return true;
                    case 'v': verbose=true; return true;
                    case 'T': noTargetDir=true; return true;
                    case 'm': modeSpec=v; return true;
                    case 'o': ownerName=v; return true;
                    case 'g': groupName=v; return true;
                    case 't': targetDir=v; return true;
                    default: return text::badOpt("install",c);
                }

            if(operands.empty()){
                std::cerr << "install: usage: install [-m MODE] SRC... DST | install -d DIR..." << std::endl;
            },[&](const std::string &n,const std::string &v,bool hasV)->int{
                if(n=="directory"){ dirMode=true; return 0; }
                if(n=="preserve-timestamps"){ preserve=true; return 0; }
                if(n=="strip"){ strip=true; return 0; }
                if(n=="verbose"){ verbose=true; return 0; }
                if(n=="compare"){ compare=true; return 0; }
                if(n=="no-target-directory"){ noTargetDir=true; return 0; }
                if(n=="backup" || n=="preserve-context" || n=="debug") return 0;
                if(n=="mode"){ if(!hasV) return 1; modeSpec=v; return 0; }
                if(n=="owner"){ if(!hasV) return 1; ownerName=v; return 0; }
                if(n=="group"){ if(!hasV) return 1; groupName=v; return 0; }
                if(n=="target-directory"){ if(!hasV) return 1; targetDir=v; return 0; }
                if(n=="strip-program"){ if(!hasV) return 1; stripProg=v; return 0; }
                if(n=="suffix" || n=="context") return hasV ? 0 : 1;
                return text::badLong("install",n);
            });
            if(!o.ok) return 1;
            mode=0755;
            if(!modeSpec.empty() && !mode::apply(modeSpec,0,dirMode,0,mode)){
                std::cerr << "install: invalid mode '" << modeSpec << "'" << std::endl;
                return 1;
            }

            if(makeDirs){
            if(!ownerName.empty()){
                struct passwd *pw=getpwnam(ownerName.c_str());
                char *e;
                unsigned long v=strtoul(ownerName.c_str(),&e,10);
                if(pw) owner=pw->pw_uid;
                else if(!*e) owner=(uid_t)v;
                else{ std::cerr << "install: invalid user '" << ownerName << "'" << std::endl; return 1; }
                haveOwner=true;
            }
            if(!groupName.empty()){
                struct group *gr=getgrnam(groupName.c_str());
                char *e;
                unsigned long v=strtoul(groupName.c_str(),&e,10);
                if(gr) group=gr->gr_gid;
                else if(!*e) group=(gid_t)v;
                else{ std::cerr << "install: invalid group '" << groupName << "'" << std::endl; return 1; }
                haveGroup=true;
            }
            auto &ops=o.operands;
            if(dirMode){
                if(ops.empty()){ std::cerr << "install: missing operand" << std::endl; return 1; }
                bool ok=true;
                for(auto &d: operands)
                    if(!makeDir(d)) ok=false;
                for(auto &d: ops) if(!mkdirs(d,mode,true)) ok=false;
                return ok ? 0 : 1;
            }

            if(operands.size()<2){
                std::cerr << "install: missing destination operand" << std::endl;
                return 1;
            if(!targetDir.empty()){
                if(ops.empty()){ std::cerr << "install: missing file operand" << std::endl; return 1; }
                if(leading && !mkdirs(targetDir,0755,false)) return 1;
                if(!isDir(targetDir)){ std::cerr << "install: target '" << targetDir << "' is not a directory" << std::endl; return 1; }
                bool ok=true;
                for(auto &s: ops) if(!installFile(s,targetDir+"/"+baseName(s))) ok=false;
                return ok ? 0 : 1;
            }

            std::string dst=operands.back();
            std::vector<std::string> srcs(operands.begin(),operands.end()-1);

            bool dstIsDir=isDir(dst);
            if(srcs.size()>1 && !dstIsDir){
                std::cerr << "install: " << dst << ": not a directory" << std::endl;
            if(ops.size()<2){
                if(ops.empty()) std::cerr << "install: missing file operand" << std::endl;
                else std::cerr << "install: missing destination file operand after '" << ops[0] << "'" << std::endl;
                return 1;
            }

            bool ok=true;
            for(auto &src: srcs){
                std::string target=dstIsDir ? (dst+"/"+baseName(src)) : dst;
                if(!copyFile(src,target,mode,preserveTimes))
                    ok=false;
            std::string dst=ops.back();
            ops.pop_back();
            if(ops.size()==1 && (noTargetDir || !isDir(dst))){
                if(leading){
                    std::string d=dirName(dst);
                    if(!d.empty() && !mkdirs(d,0755,false)) return 1;
                }
                return installFile(ops[0],dst) ? 0 : 1;
            }
            if(noTargetDir){ std::cerr << "install: extra operand '" << dst << "'" << std::endl; return 1; }
            if(!isDir(dst)){ std::cerr << "install: target '" << dst << "' is not a directory" << std::endl; return 1; }
            bool ok=true;
            for(auto &s: ops) if(!installFile(s,dst+"/"+baseName(s))) ok=false;
            return ok ? 0 : 1;
        }
    };