笨辦法學C 練習26:編寫第一個真正的程序

練習26:編寫第一個真正的程序

原文:Exercise 26: Write A First Real Programhtml

譯者:飛龍git

這本書你已經完成一半了,因此你須要作一個期中檢測。期中檢測中你須要從新構建一個我特意爲本書編寫的軟件,叫作devpkg。隨後你須要以一些方式擴展它,而且經過編寫一些單元測試來改進代碼。程序員

github

我在一些你須要完成的練習以前編寫了這個練習。若是你如今嘗試這個練習,記住軟件可能會含有一些bug,你可能因爲個人錯誤會產生一些問題,也可能不知道須要什麼來完成它。若是這樣的話,經過help@learncodethehardway.org來告訴我,以後等待我寫完其它練習。shell

什麼是devpkg

devpkg是一個簡單的C程序,能夠用於安裝其它軟件。我特意爲本書編寫了它,做爲一種方式來教你真正的軟件是如何構建的,以及如何複用他人的庫。它使用了一個叫作Apache可移植運行時(APR)的庫,其中含有許多工做跨平臺的便利的C函數,包括Windows。此外,它只是從互聯網(或本地文件)抓取代碼,而且執行一般的./configure ; make ; make install命令,每一個程序員都用到過。數據庫

這個練習中,你的目標是從源碼構建devpkg,完成我提供的每一個挑戰,而且使用源碼來理解devpkg作了什麼和爲何這樣作。apache

咱們打算建立什麼

咱們打算建立一個具備三個命令的工具:編程

devpkg -Sbash

在電腦上安裝新的軟件。數據結構

devpkg -I

從URL安裝軟件。

devpkg -L

列出安裝的全部軟件。

devpkg -F

爲手動構建抓取源代碼。

devpkg -B

構建所抓取的源碼代碼而且安裝它,即便它已經安裝了。

咱們想讓devpkg可以接受幾乎任何URL,判斷項目的類型,下載,安裝,以及註冊已經安裝的軟件。咱們也但願它可以處理一個簡單的依賴列表,以便它可以安裝項目所需的全部軟件。

設計

爲了完成這一目標,devpkg具備很是簡單的設計:

使用外部命令

大多數工做都是經過相似於curlgittar的外部命令完成的。這樣減小了devpkg所需的代碼量。

簡單的文件數據庫

你能夠輕易使它變得很複雜,可是一開始你須要完成一個簡單的文件數據庫,位於/usr/local/.devpkg/db,來跟蹤已安裝的軟件。

/usr/local

一樣你可使它更高級,可是對於初學者來講,假設項目始終位於/usr/local中,它是愛多數Unix軟件的標準安裝目錄。

configure; make; make install

假設大多數軟件能夠經過configure; make; make install來安裝,也許configure是可選的。若是軟件不能經過這種方式安裝,要麼提供某種方式來修改命令,要麼devpkg就能夠無視它。

用戶能夠root

咱們假設用於可使用sudo來提高至root權限,除非他們直到最後纔想root。

這會使咱們的程序像當初設想的同樣簡單,而且對於它的功能來講已經足夠了。以後你能夠進一步修改它。

Apache 可移植運行時

你須要作的另一件事情就是使用Apache可移植運行時(APR)來未完成這個練習得到一個可移植的工具集。APR並非必要的,你也能夠不用它,可是你須要寫的代碼就會很是多。我如今強制你使用APR,使你可以熟悉連接和使用其餘的庫。最後,APR也能在Windows上工做,因此你能夠把它遷移到許多其它平臺上。

你應該獲取apr-1.4.5apr-util-1.3的庫,以及瀏覽在apr.apache.org主站上的文檔。

下面是一個ShellScript,用於安裝所需的全部庫。你應該手動將它寫到一個文件中,以後運行它直到APR安裝好而且沒有任何錯誤。

set -e

# go somewhere safe
cd /tmp

# get the source to base APR 1.4.6
curl -L -O http://archive.apache.org/dist/apr/apr-1.4.6.tar.gz

# extract it and go into the source
tar -xzvf apr-1.4.6.tar.gz
cd apr-1.4.6

# configure, make, make install
./configure
make
sudo make install

# reset and cleanup
cd /tmp
rm -rf apr-1.4.6 apr-1.4.6.tar.gz

# do the same with apr-util
curl -L -O http://archive.apache.org/dist/apr/apr-util-1.4.1.tar.gz

# extract
tar -xzvf apr-util-1.4.1.tar.gz
cd apr-util-1.4.1

# configure, make, make install
./configure --with-apr=/usr/local/apr
# you need that extra parameter to configure because
# apr-util can't really find it because...who knows.

make
sudo make install

#cleanup
cd /tmp
rm -rf apr-util-1.4.1* apr-1.4.6*

我但願你輸入這個腳本,由於這就是devpkg基本上所作的事情,只是帶有了一些選項和檢查項。實際上,你可使用Shell以更少的代碼來完成它,可是這對於一本C語言的書不是一個很好的程序。

簡單運行這個腳本,修復它直到正常工做,就完成的全部庫的安裝,以後你須要完成項目的剩下部分。

項目佈局

你須要建立一些簡單的項目文件來起步。下面是我一般建立一個新項目的方法:

mkdir devpkg
cd devpkg
touch README Makefile

其它依賴

你應該已經安裝了APR和APR-util,因此你須要一些更多的文件做爲基本的依賴:

  • 練習20中的dbg.h

  • http://bstring.sourceforge.net/下載的bstrlib.hbstrlib.c。下載.zip文件,解壓而且將這個兩個文件拷貝到項目中。

  • 運行make bstrlib.o,若是這不能正常工做,閱讀下面的「修復bstring」指南。

在一些平臺上bstring.c文件會出現下列錯誤:

bstrlib.c:2762: error: expected declaration specifiers or '...' before numeric constant

這是因爲做者使用了一個很差的定義,它在一些平臺上不能工做。你須要修改第2759行的#ifdef __GNUC__,並把它改爲:

#if defined(__GNUC__) && !defined(__APPLE__)

以後在Mac OSX平臺上就應該可以正常工做了。

作完上面這些後,你應該有了MakefileREADMEdbg.hbstrlib.hbstrlib.c,並作好了準備。

Makefile

咱們最好從Makefile開始,由於它列出了項目如何構建,以及你會建立哪些源文件。

PREFIX?=/usr/local
CFLAGS=-g -Wall -I${PREFIX}/apr/include/apr-1  -I${PREFIX}/apr/include/apr-util-1
LDFLAGS=-L${PREFIX}/apr/lib -lapr-1 -pthread -laprutil-1

all: devpkg

devpkg: bstrlib.o db.o shell.o commands.o

install: all
     install -d $(DESTDIR)/$(PREFIX)/bin/
     install devpkg $(DESTDIR)/$(PREFIX)/bin/

clean:
     rm -f *.o
     rm -f devpkg
     rm -rf *.dSYM

比起以前看到過的,這並無什麼新東西,除了可能有些奇怪的?=語法,它表示「若是以前沒有定義,就將PREFIX設置爲該值」。

若是你使用了最近版本的Ubuntu,你會獲得apr_off_toff64_t的錯誤,以後須要向CFLAGS添加-D_LARGEFILE64_SOURCE=1

所需的另外一件事是,你須要向/etc/ld.conf.so.d/添加/usr/local/apr/lib,以後運行ldconfig使它可以選擇正常的庫。

源文件

咱們能夠從makefile中看到,devpkg有四個依賴項,它們是:

bstrlib.o

bstrlib.cbstrlib.o產生,你已經將它們引入了。

db.o

db.cdb.h產生,它包含了一個小型「數據庫」程序集的代碼。

shell.o

shell.cshell.h產生,包含一些函數,是相似curl的一些命令運行起來更容易。

commands.o

commands.ccommands.h產生,包含了devpkg所需的全部命令並使它更易用。

devpkg

它不會顯式提到,可是它是Makefile在這一部分的目標。它由devpkg.c產生,包含用於整個程序的main函數。

你的任務就是建立這些文件,而且輸入代碼並保證正確。

你讀完這個描述可能會想,「Zed爲何那麼聰明,坐着就能設計出來這些文件?!」我並非用我強大的代碼功力魔術般地把devpkg設計成這樣。而是我作了這些:

  • 我編寫了簡單的README來得到如何構建項目的靈感。

  • 我建立了一個簡單的bash腳本(就像你編寫的那樣)來理清所需的全部組件。

  • 我建立了一個.c文件,而且在它上面花了幾天,醞釀並想出點子。

  • 接着我編寫並調試程序,以後我將這一個大文件分紅四個文件。

  • 作完這些以後,我重命名和優化了函數和數據結構,使它們在邏輯上更「美觀」。

  • 最後,使新程序成功並以相同方式工做以後,我添加了一些新的特性,好比-F-B選項。

你讀到的這份列表是我打算教給你的,但不要認爲這是我構建軟件的通用方法。有時候我會事先知道主題,而且會作更多的規劃。也有時我會編寫一份規劃並將它扔掉,以後再規劃更好的版本。它徹底取決於個人經驗告訴我哪一個比較好,或者個人靈感將我帶到何處。

若是你碰到一個「專家」,它告訴你只有一個方法能夠解決編程問題,那麼它在騙你。要麼它們實際使用了不少策略,要麼他們並不足夠好。

DB函數

程序中必須有個方法來記錄已經安裝的URL,列出這些URL,而且檢查一些程序是否已安裝以便跳過。我會使用一個簡單、扁平化的文件數據庫,以及bstrlib.h

首先,建立db.h頭文件,以便讓你知道須要實現什麼。

#ifndef _db_h
#define _db_h

#define DB_FILE "/usr/local/.devpkg/db"
#define DB_DIR "/usr/local/.devpkg"


int DB_init();
int DB_list();
int DB_update(const char *url);
int DB_find(const char *url);

#endif

以後實現db.c中的這些函數,在你編寫它的時候,像以前同樣使用make

#include <unistd.h>
#include <apr_errno.h>
#include <apr_file_io.h>

#include "db.h"
#include "bstrlib.h"
#include "dbg.h"

static FILE *DB_open(const char *path, const char *mode)
{
    return fopen(path, mode);
}


static void DB_close(FILE *db)
{
    fclose(db);
}


static bstring DB_load()
{
    FILE *db = NULL;
    bstring data = NULL;

    db = DB_open(DB_FILE, "r");
    check(db, "Failed to open database: %s", DB_FILE);

    data = bread((bNread)fread, db);
    check(data, "Failed to read from db file: %s", DB_FILE);

    DB_close(db);
    return data;

error:
    if(db) DB_close(db);
    if(data) bdestroy(data);
    return NULL;
}


int DB_update(const char *url)
{
    if(DB_find(url)) {
        log_info("Already recorded as installed: %s", url);
    }

    FILE *db = DB_open(DB_FILE, "a+");
    check(db, "Failed to open DB file: %s", DB_FILE);

    bstring line = bfromcstr(url);
    bconchar(line, '\n');
    int rc = fwrite(line->data, blength(line), 1, db);
    check(rc == 1, "Failed to append to the db.");

    return 0;
error:
    if(db) DB_close(db);
    return -1;
}


int DB_find(const char *url)
{
    bstring data = NULL;
    bstring line = bfromcstr(url);
    int res = -1;

    data = DB_load();
    check(data, "Failed to load: %s", DB_FILE);

    if(binstr(data, 0, line) == BSTR_ERR) {
        res = 0;
    } else {
        res = 1;
    }

error: // fallthrough
    if(data) bdestroy(data);
    if(line) bdestroy(line);

    return res;
}


int DB_init()
{
    apr_pool_t *p = NULL;
    apr_pool_initialize();
    apr_pool_create(&p, NULL);

    if(access(DB_DIR, W_OK | X_OK) == -1) {
        apr_status_t rc = apr_dir_make_recursive(DB_DIR,
                APR_UREAD | APR_UWRITE | APR_UEXECUTE |
                APR_GREAD | APR_GWRITE | APR_GEXECUTE, p);
        check(rc == APR_SUCCESS, "Failed to make database dir: %s", DB_DIR);
    }

    if(access(DB_FILE, W_OK) == -1) {
        FILE *db = DB_open(DB_FILE, "w");
        check(db, "Cannot open database: %s", DB_FILE);
        DB_close(db);
    }

    apr_pool_destroy(p);
    return 0;

error:
    apr_pool_destroy(p);
    return -1;
}


int DB_list()
{
    bstring data = DB_load();
    check(data, "Failed to read load: %s", DB_FILE);

    printf("%s", bdata(data));
    bdestroy(data);
    return 0;

error:
    return -1;
}

挑戰1:代碼複查

在繼續以前,仔細閱讀這些文件的每一行,而且確保你以準確地輸入了它們。經過逐行閱讀代碼來實踐它。同時,跟蹤每一個函數調用,而且確保你使用了check來校驗返回值。最後,在APR網站上的文檔,或者bstrlib.h 或 bstrlib.c的源碼中,查閱每一個你不認識的函數。

Shell 函數

devkpg的一個關鍵設計是,使用相似於curltargit的外部工具來完成大部分的工做。咱們能夠找到在程序內部完成這些工做的庫,可是若是咱們只是須要這些程序的基本功能,這樣就毫無心義。在Unix運行其它命令並不丟人。

爲了完成這些,我打算使用apr_thread_proc.h函數來運行程序,可是我也但願建立一個簡單的類「模板」系統。我會使用struct Shell,它持有全部運行程序所需的信息,可是在參數中有一些「空位」,我能夠將它們替換成實際值。

觀察shell.h文件來了解我會用到的結構和命令。你能夠看到我使用extern來代表其餘的.c文件也能訪問到shell.c中定義的變量。

#ifndef _shell_h
#define _shell_h

#define MAX_COMMAND_ARGS 100

#include <apr_thread_proc.h>

typedef struct Shell {
    const char *dir;
    const char *exe;

    apr_procattr_t *attr;
    apr_proc_t proc;
    apr_exit_why_e exit_why;
    int exit_code;

    const char *args[MAX_COMMAND_ARGS];
} Shell;

int Shell_run(apr_pool_t *p, Shell *cmd);
int Shell_exec(Shell cmd, ...);

extern Shell CLEANUP_SH;
extern Shell GIT_SH;
extern Shell TAR_SH;
extern Shell CURL_SH;
extern Shell CONFIGURE_SH;
extern Shell MAKE_SH;
extern Shell INSTALL_SH;

#endif

確保你已經建立了shell.h,而且extern Shell變量的名字和數量相同。它們被Shell_runShell_exec函數用於運行命令。我定義了這兩個函數,而且在shell.c中建立實際變量。

#include "shell.h"
#include "dbg.h"
#include <stdarg.h>

int Shell_exec(Shell template, ...)
{
    apr_pool_t *p = NULL;
    int rc = -1;
    apr_status_t rv = APR_SUCCESS;
    va_list argp;
    const char *key = NULL;
    const char *arg = NULL;
    int i = 0;

    rv = apr_pool_create(&p, NULL);
    check(rv == APR_SUCCESS, "Failed to create pool.");

    va_start(argp, template);

    for(key = va_arg(argp, const char *);
        key != NULL;
        key = va_arg(argp, const char *))
    {
        arg = va_arg(argp, const char *);

        for(i = 0; template.args[i] != NULL; i++) {
            if(strcmp(template.args[i], key) == 0) {
                template.args[i] = arg;
                break; // found it
            }
        }
    }

    rc = Shell_run(p, &template);
    apr_pool_destroy(p);
    va_end(argp);
    return rc;

error:
    if(p) {
        apr_pool_destroy(p);
    }
    return rc;
}

int Shell_run(apr_pool_t *p, Shell *cmd)
{
    apr_procattr_t *attr;
    apr_status_t rv;
    apr_proc_t newproc;

    rv = apr_procattr_create(&attr, p);
    check(rv == APR_SUCCESS, "Failed to create proc attr.");

    rv = apr_procattr_io_set(attr, APR_NO_PIPE, APR_NO_PIPE,
            APR_NO_PIPE);
    check(rv == APR_SUCCESS, "Failed to set IO of command.");

    rv = apr_procattr_dir_set(attr, cmd->dir);
    check(rv == APR_SUCCESS, "Failed to set root to %s", cmd->dir);

    rv = apr_procattr_cmdtype_set(attr, APR_PROGRAM_PATH);
    check(rv == APR_SUCCESS, "Failed to set cmd type.");

    rv = apr_proc_create(&newproc, cmd->exe, cmd->args, NULL, attr, p);
    check(rv == APR_SUCCESS, "Failed to run command.");

    rv = apr_proc_wait(&newproc, &cmd->exit_code, &cmd->exit_why, APR_WAIT);
    check(rv == APR_CHILD_DONE, "Failed to wait.");

    check(cmd->exit_code == 0, "%s exited badly.", cmd->exe);
    check(cmd->exit_why == APR_PROC_EXIT, "%s was killed or crashed", cmd->exe);

    return 0;

error:
    return -1;
}

Shell CLEANUP_SH = {
    .exe = "rm",
    .dir = "/tmp",
    .args = {"rm", "-rf", "/tmp/pkg-build", "/tmp/pkg-src.tar.gz",
        "/tmp/pkg-src.tar.bz2", "/tmp/DEPENDS", NULL}
};

Shell GIT_SH = {
    .dir = "/tmp",
    .exe = "git",
    .args = {"git", "clone", "URL", "pkg-build", NULL}
};

Shell TAR_SH = {
    .dir = "/tmp/pkg-build",
    .exe = "tar",
    .args = {"tar", "-xzf", "FILE", "--strip-components", "1", NULL}
};

Shell CURL_SH = {
    .dir = "/tmp",
    .exe = "curl",
    .args = {"curl", "-L", "-o", "TARGET", "URL", NULL}
};

Shell CONFIGURE_SH = {
    .exe = "./configure",
    .dir = "/tmp/pkg-build",
    .args = {"configure", "OPTS", NULL},
};

Shell MAKE_SH = {
    .exe = "make",
    .dir = "/tmp/pkg-build",
    .args = {"make", "OPTS", NULL}
};

Shell INSTALL_SH = {
    .exe = "sudo",
    .dir = "/tmp/pkg-build",
    .args = {"sudo", "make", "TARGET", NULL}
};

自底向上閱讀shell.c的代碼(這也是常見的C源碼佈局),你會看到我建立了實際的Shell變量,它在shell.h中以extern修飾。它們雖然在這裏,可是也被程序的其它部分使用。這就是建立全局變量的方式,它們能夠存在於一個.c文件中,可是可在任何地方使用。你不該該建立不少這類變量,可是它們的確很方便。

繼續閱讀代碼,咱們讀到了Shell_run,它是一個「基」函數,只是基於Shell中的東西執行命令。它使用了許多在apr_thread_proc.h中定義的函數,你須要查閱它們的每個來了解工做原理。這就像是一些使用system函數調用的代碼同樣,可是它可讓你控制其餘程序的執行。例如,在咱們的Shell結構中,存在.dir屬性在運行以前強制程序必須在指定目錄中。

最後,我建立了Shell_exec函數,它是個變參函數。你在以前已經看到過了,可是確保你理解了stdarg.h函數以及如何編寫它們。在下個挑戰中你須要分析這一函數。

挑戰2:分析Shell_exec

爲這些文件(以及向挑戰1那樣的完整的代碼複查)設置的挑戰是完整分析Shell_exec,而且拆分代碼來了解工做原理。你應該可以理解每一行代碼,for循環如何工做,以及參數如何被替換。

一旦你分析完成,向struct Shell添加一個字段,提供須要替代的args變量的數量。更新全部命令來接受參數的正確數量,隨後增長一個錯誤檢查,來確認參數被正確替換,以及在錯誤時退出。

命令行函數

如今你須要構造正確的命令來完成功能。這些命令會用到APR的函數、db.hshell.h來執行下載和構建軟件的真正工做。這些文件最爲複雜,因此要當心編寫它們。你須要首先編寫commands.h文件,接着在commands.c文件中實現它的函數。

#ifndef _commands_h
#define _commands_h

#include <apr_pools.h>

#define DEPENDS_PATH "/tmp/DEPENDS"
#define TAR_GZ_SRC "/tmp/pkg-src.tar.gz"
#define TAR_BZ2_SRC "/tmp/pkg-src.tar.bz2"
#define BUILD_DIR "/tmp/pkg-build"
#define GIT_PAT "*.git"
#define DEPEND_PAT "*DEPENDS"
#define TAR_GZ_PAT "*.tar.gz"
#define TAR_BZ2_PAT "*.tar.bz2"
#define CONFIG_SCRIPT "/tmp/pkg-build/configure"

enum CommandType {
    COMMAND_NONE, COMMAND_INSTALL, COMMAND_LIST, COMMAND_FETCH,
    COMMAND_INIT, COMMAND_BUILD
};


int Command_fetch(apr_pool_t *p, const char *url, int fetch_only);

int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts);

int Command_depends(apr_pool_t *p, const char *path);

int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts);

#endif

commands.h中並無不少以前沒見過的東西。你應該看到了一些字符串的定義,它們在任何地方都會用到。真正的代碼在commands.c中。

#include <apr_uri.h>
#include <apr_fnmatch.h>
#include <unistd.h>

#include "commands.h"
#include "dbg.h"
#include "bstrlib.h"
#include "db.h"
#include "shell.h"


int Command_depends(apr_pool_t *p, const char *path)
{
    FILE *in = NULL;
    bstring line = NULL;

    in = fopen(path, "r");
    check(in != NULL, "Failed to open downloaded depends: %s", path);

    for(line = bgets((bNgetc)fgetc, in, '\n'); line != NULL;
            line = bgets((bNgetc)fgetc, in, '\n'))
    {
        btrimws(line);
        log_info("Processing depends: %s", bdata(line));
        int rc = Command_install(p, bdata(line), NULL, NULL, NULL);
        check(rc == 0, "Failed to install: %s", bdata(line));
        bdestroy(line);
    }

    fclose(in);
    return 0;

error:
    if(line) bdestroy(line);
    if(in) fclose(in);
    return -1;
}

int Command_fetch(apr_pool_t *p, const char *url, int fetch_only)
{
    apr_uri_t info = {.port = 0};
    int rc = 0;
    const char *depends_file = NULL;
    apr_status_t rv = apr_uri_parse(p, url, &info);

    check(rv == APR_SUCCESS, "Failed to parse URL: %s", url);

    if(apr_fnmatch(GIT_PAT, info.path, 0) == APR_SUCCESS) {
        rc = Shell_exec(GIT_SH, "URL", url, NULL);
        check(rc == 0, "git failed.");
    } else if(apr_fnmatch(DEPEND_PAT, info.path, 0) == APR_SUCCESS) {
        check(!fetch_only, "No point in fetching a DEPENDS file.");

        if(info.scheme) {
            depends_file = DEPENDS_PATH;
            rc = Shell_exec(CURL_SH, "URL", url, "TARGET", depends_file, NULL);
            check(rc == 0, "Curl failed.");
        } else {
            depends_file = info.path;
        }

        // recursively process the devpkg list
        log_info("Building according to DEPENDS: %s", url);
        rv = Command_depends(p, depends_file);
        check(rv == 0, "Failed to process the DEPENDS: %s", url);

        // this indicates that nothing needs to be done
        return 0;

    } else if(apr_fnmatch(TAR_GZ_PAT, info.path, 0) == APR_SUCCESS) {
        if(info.scheme) {
            rc = Shell_exec(CURL_SH,
                    "URL", url,
                    "TARGET", TAR_GZ_SRC, NULL);
            check(rc == 0, "Failed to curl source: %s", url);
        }

        rv = apr_dir_make_recursive(BUILD_DIR,
                APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);
        check(rv == APR_SUCCESS, "Failed to make directory %s", BUILD_DIR);

        rc = Shell_exec(TAR_SH, "FILE", TAR_GZ_SRC, NULL);
        check(rc == 0, "Failed to untar %s", TAR_GZ_SRC);
    } else if(apr_fnmatch(TAR_BZ2_PAT, info.path, 0) == APR_SUCCESS) {
        if(info.scheme) {
            rc = Shell_exec(CURL_SH, "URL", url, "TARGET", TAR_BZ2_SRC, NULL);
            check(rc == 0, "Curl failed.");
        }

        apr_status_t rc = apr_dir_make_recursive(BUILD_DIR,
                APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);

        check(rc == 0, "Failed to make directory %s", BUILD_DIR);
        rc = Shell_exec(TAR_SH, "FILE", TAR_BZ2_SRC, NULL);
        check(rc == 0, "Failed to untar %s", TAR_BZ2_SRC);
    } else {
        sentinel("Don't now how to handle %s", url);
    }

    // indicates that an install needs to actually run
    return 1;
error:
    return -1;
}

int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts)
{
    int rc = 0;

    check(access(BUILD_DIR, X_OK | R_OK | W_OK) == 0,
            "Build directory doesn't exist: %s", BUILD_DIR);

    // actually do an install
    if(access(CONFIG_SCRIPT, X_OK) == 0) {
        log_info("Has a configure script, running it.");
        rc = Shell_exec(CONFIGURE_SH, "OPTS", configure_opts, NULL);
        check(rc == 0, "Failed to configure.");
    }

    rc = Shell_exec(MAKE_SH, "OPTS", make_opts, NULL);
    check(rc == 0, "Failed to build.");

    rc = Shell_exec(INSTALL_SH,
            "TARGET", install_opts ? install_opts : "install",
            NULL);
    check(rc == 0, "Failed to install.");

    rc = Shell_exec(CLEANUP_SH, NULL);
    check(rc == 0, "Failed to cleanup after build.");

    rc = DB_update(url);
    check(rc == 0, "Failed to add this package to the database.");

    return 0;

error:
    return -1;
}

int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts)
{
    int rc = 0;
    check(Shell_exec(CLEANUP_SH, NULL) == 0, "Failed to cleanup before building.");

    rc = DB_find(url);
    check(rc != -1, "Error checking the install database.");

    if(rc == 1) {
        log_info("Package %s already installed.", url);
        return 0;
    }

    rc = Command_fetch(p, url, 0);

    if(rc == 1) {
        rc = Command_build(p, url, configure_opts, make_opts, install_opts);
        check(rc == 0, "Failed to build: %s", url);
    } else if(rc == 0) {
        // no install needed
        log_info("Depends successfully installed: %s", url);
    } else {
        // had an error
        sentinel("Install failed: %s", url);
    }

    Shell_exec(CLEANUP_SH, NULL);
    return 0;

error:
    Shell_exec(CLEANUP_SH, NULL);
    return -1;
}

在你輸入並編譯它以後,就能夠開始分析了。若是到目前爲止你完成了前面的挑戰,你會理解如何使用shell.c函數來運行shell命令,以及參數如何被替換。若是沒有則須要回退到前面的挑戰,確保你真正理解了Shell_exec的工做原理。

挑戰3:評判個人設計

像以前同樣,完整地複查一遍代碼來保證如出一轍。接着瀏覽每一個函數而且確保你知道他如何工做。你也應該跟蹤這個文件或其它文件中,每一個函數對其它函數的調用。最後,確認你理解了這裏的全部調用APR的函數。

一旦你正確編寫並分析了這個文件,把我當成一個傻瓜同樣來評判個人設計,我須要看看你是否能夠改進它。不要真正修改代碼,只是建立一個notes.txt而且寫下你的想法和你須要修改的地方。

devpkgmain函數

devpkg.c是最後且最重要的,可是也多是最簡單的文件,其中建立了main函數。沒有與之配套的.h文件,由於這個文件包含其餘全部文件。這個文件用於建立devpkg可執行程序,同時組裝了來自Makefile的其它.o文件。在文件中輸入代碼並保證正確。

#include <stdio.h>
#include <apr_general.h>
#include <apr_getopt.h>
#include <apr_strings.h>
#include <apr_lib.h>

#include "dbg.h"
#include "db.h"
#include "commands.h"

int main(int argc, const char const *argv[])
{
    apr_pool_t *p = NULL;
    apr_pool_initialize();
    apr_pool_create(&p, NULL);

    apr_getopt_t *opt;
    apr_status_t rv;

    char ch = '\0';
    const char *optarg = NULL;
    const char *config_opts = NULL;
    const char *install_opts = NULL;
    const char *make_opts = NULL;
    const char *url = NULL;
    enum CommandType request = COMMAND_NONE;


    rv = apr_getopt_init(&opt, p, argc, argv);

    while(apr_getopt(opt, "I:Lc:m:i:d:SF:B:", &ch, &optarg) == APR_SUCCESS) {
        switch (ch) {
            case 'I':
                request = COMMAND_INSTALL;
                url = optarg;
                break;

            case 'L':
                request = COMMAND_LIST;
                break;

            case 'c':
                config_opts = optarg;
                break;

            case 'm':
                make_opts = optarg;
                break;

            case 'i':
                install_opts = optarg;
                break;

            case 'S':
                request = COMMAND_INIT;
                break;

            case 'F':
                request = COMMAND_FETCH;
                url = optarg;
                break;

            case 'B':
                request = COMMAND_BUILD;
                url = optarg;
                break;
        }
    }

    switch(request) {
        case COMMAND_INSTALL:
            check(url, "You must at least give a URL.");
            Command_install(p, url, config_opts, make_opts, install_opts);
            break;

        case COMMAND_LIST:
            DB_list();
            break;

        case COMMAND_FETCH:
            check(url != NULL, "You must give a URL.");
            Command_fetch(p, url, 1);
            log_info("Downloaded to %s and in /tmp/", BUILD_DIR);
            break;

        case COMMAND_BUILD:
            check(url, "You must at least give a URL.");
            Command_build(p, url, config_opts, make_opts, install_opts);
            break;

        case COMMAND_INIT:
            rv = DB_init();
            check(rv == 0, "Failed to make the database.");
            break;

        default:
            sentinel("Invalid command given.");
    }


    return 0;

error:
    return 1;
}

爲這個文件設置的挑戰是理解參數如何處理,以及參數是什麼,以後建立含有使用指南的README文件。在編寫README的同時,也編寫一個簡單的simple.sh,它運行./devpkg來檢查每一個命令都在實際環境下工做。在你的腳本頂端使用set -e`,使它跳過第一個錯誤。

最後,在Valgrind下運行程序,確保在進行下一步以前,全部東西都能正常運行。

期中檢測

最後的挑戰就是這個期中檢測,它包含三件事情:

  • 將你的代碼與個人在線代碼對比,以100%的分數開始,每錯一行減去1%。

  • 在你的notes.txt中記錄你是如何改進代碼和devpkg的功能,而且實現你的改進。

  • 編寫一個devpkg的替代版本,使用其餘你喜歡的語言,或者你以爲最適合編寫它的語言。對比兩者,以後基於你的結果改進你的devpkg的C版本。

你能夠執行下列命令來將你的代碼與個人對比:

cd ..  # get one directory above your current one
git clone git://gitorious.org/devpkg/devpkg.git devpkgzed
diff -r devpkg devpkgzed

這將會克隆個人devpkg版本到devpkgzed目錄中。以後使用工具diff來對比你的和個人代碼。書中你所使用的這些文件直接來自於這個項目,因此若是出現了不一樣的行,確定就有錯誤。

要記住這個練習沒有真正的及格或不及格,它只是一個方式來讓你挑戰本身,並儘量變得精確和謹慎。

相關文章
相關標籤/搜索