ソフトウェア設計論

まつ本

モダンSW開発

モダンSW開発 -ビルド-


目次

・ビルド?建築?
・ビルドツール
・Gradleを試す

・依存関係
・依存の衝突
・Pythonのパッケージ管理事情
・演習✍️️

・ビルドツールの歴史
・ビルドツールと版管理
"X as a Code"
・SW開発外でのパッケージ管理

ビルド?建築?

ソースから実行ファイルを得るプロセス

.c.o .exe .app
.java.class .jar
.py.pyc .whl
.tex.pdf

ビルドの中心的な作業はコンパイル

C言語 gcc / Java javac / Python compileall

SWは最終的にビルドされてリリースされる

リリースとはソースコードの配布ではない
利用者目線ではビルドの結果だけがほしい

ビルドの難しさ

ビルドは本当に大変

単なるコンパイルだけではない

  • 依存管理:依存関係の解決 / 依存libの取得
  • 静的解析:脆弱性解析 / 形式チェック / リンタ
  • コンパイル:gcc / javac / compileall
  • テスト:単体・全体の検証 / テスト環境の整備
  • リリース:zipの作成 / 配布サーバへの設置

開発者本人はビルドできるかもしれない

開発者以外は?1年後の自分自身は?

上記のビルド作業をREADMEに記載するのか?
本当に読むか?手で再現できるか?メンテは?

javacの難しさ

ただのコンパイルでも様々な指定が必要

$ javac \
  --release 23 \     # jreのバージョン指定
  -encoding UTF-8 \  # エンコード指定
  -g \               # デバッグ情報を付与
  -Xlint:all \       # コンパイル警告を表示
  -d build/classes \ # 出力先を指定
  -classpath lib/*.jar:external/*.jar:... \  # lib指定
  -processorpath lib/lombok.jar \ # アノテーションプロセッサ指定
  -Amapstruct.defaultComponentModel=spring \
  --module-path modules $(find src/main/java -name "*.java")
  ...

1コマンドにできないか?

$ <tool> compile

ビルドツール

ビルドを自動化する仕組み

ビルド作業そのものを機械に実行させる
同じ手順の再現は機械が得意な分野

ビルドの各種構成を静的なファイルに切り出す

ビルドは人手でやるべきではない

ミスの温床 / ソースのバグ?ビルドのバグ?

各種言語にビルドツールが存在する

C:make
Java:Ant / Maven / Gradle
Python:PyBuilder / Setuptools / uv
LaTeX:latexmk

Gradleの場合

Gradleによるビルド

$ ./gradlew build
BUILD SUCCESSFUL in 10s

上記buildタスクの依存関係

flowchart LR
  classes --> compileJava & processResources
  javadoc --> classes
  compileTestJava --> classes
  processTestResrouces --> classes
  jar --> classes
  testClasses --> compileTestJava & processTestResrouces
  uploadArchives --> jar
  assemble --> jar
  test --> testClasses & classes
  check --> test
  build --> check & assemble

IDEを使えばよいのでは?

雑談

最近のIDEは賢い

ビルド (の一部) はIDEが助けてくれる
特にコンパイル / テスト / リンタ等

最初に設定しておいて後はIDEに任す
ボタン一つでビルド (の一部) を実行できる

しかしIDEなしでビルドしたい状況も多々ある

例1:テストだけ動かしたい
テストのためだけにIDEを立ち上げるのか?
IDEの主目的は開発でありビルドではない

例2:サーバ上でビルドしたい
次回の講義CI/CDへ

Gradleを試す

プロジェクトの生成

$ mkdir gradle-trial && cd gradle-trial
$ gradle init  # gradleプロジェクトの生成

Welcome to Gradle 8.14!
...
Select type of build to generate:
  1: Application
  2: Library
  3: Gradle plugin
  4: Basic (build structure only)
Enter selection (default: Application) [1..4]
...

各種設定 (規約) の変更を対話的に行う
変更がなければEnter連打 (ここではApplicationを選択)

生成結果の確認

$ ls -p
app/  gradle/  gradle.properties  gradlew  ...

$ tree app  # appの中身を確認
app
├── build.gradle.kts  # アプリビルド方法の規約 (依存宣言等)  
└── src               # src以下がソースコード
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── example
    │   │           └── App.java      # プロダクトコード
    │   └── resources
    └── test
        ├── java
        │   └── org
        │       └── example
        │           └── AppTest.java  # テストコード
        └── resources

ソースコードを確認

$ cat app/src/main/java/org/example/App.java
package org.example;

public class App {
    public String getGreeting() {
        return "Hello World!";
    }

    public static void main(String[] args) {
        System.out.println(new App().getGreeting());
    }
}

Javaのサンプルコードが生成されている

テストコードを確認

$ cat app/src/test/java/org/example/AppTest.java
package org.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class AppTest {
    @Test void appHasAGreeting() {
        App classUnderTest = new App();
        assertNotNull(classUnderTest.getGreeting(),
          "app should have a greeting");
    }
}

Javaのサンプルテストコードが生成されている

最小限のJavaプロジェクトが生成できた
あとはIDEを使って開発を進めれば良い

ビルドする

$ ./gradlew build
BUILD SUCCESSFUL in 9s

結果を確認

$ tree app/build
app/build
├── classes
│   └── java
│       ├── main
│       │   └── org
│       │       └── example
│       :           └── App.class  # コンパイル結果
├── libs
│   └── app.jar  # 配布版
├── reports
│   └ ⋯
│           ├── classes       # ↓ テスト結果
│           │   └── org.example.AppTest.html

軽くGradleの裏側を確認

app以外の生成ディレクトリを確認

$ tree
.
├── app
│   ├──⋯
│   :
├── gradle
│   ├── libs.versions.toml  # バージョンカタログ (略)
│   └── wrapper                       # wrapper関係
│       ├── gradle-wrapper.jar        # wrapper関係
│       └── gradle-wrapper.properties # wrapper関係
├── gradle.properties                 # wrapper関係
├── gradlew                           # wrapper関係
├── gradlew.bat                       # wrapper関係
└── settings.gradle.kts  # プロジェクト全体の設定 (略)

wrapper周りのファイルが多数生成されている

wrapperって?

Gradle未インストールでもGradleを利用可能

$ gradle build     # インストール済みのgradleを使ってビルド
BUILD SUCCESSFUL in 754ms
$ ./gradlew build  # wrapperを使ってビルド (↑と同じ結果)
BUILD SUCCESSFUL in 754ms

プロジェクトの環境依存を最小限にできる
開発者が指定したverのgradleを実行できる

裏側ではgradleのjarをDLして実行してるだけ

$ ./gradlew build
Downloading https:.../gradle-8.14-bin.zip
.............10%.............20%.........   # DL開始
BUILD SUCCESSFUL in 12s

インストールと実行

雑談

"インストールしてから実行" という常識

WindowsもAndroidもMacもLinuxも同じ

しかしインストールは目的ではない

インストールは目的のための手段
真の目的は実行

実行時に暗黙的にインストールする 💡

利用者は目的を指示する $ ./gradlew build
npxやpipxも似たアイデア $ npx tree-node-cli
Webアプリも似ている http://maps.google.com

今後似た仕組みが増えるかもしれない

モダンSW開発 -ビルド-


目次

・ビルド?建築?
・ビルドツール
・Gradleを試す

・依存関係
・依存の衝突
・Pythonのパッケージ管理事情
・演習✍️️

・ビルドツールの歴史
・ビルドツールと版管理
"X as a Code"
・SW開発外でのパッケージ管理

依存関係の解決

Dependency resolution

ビルド作業の一部
プログラム間の依存関係を認識して解決する

依存の例 (Pythonプロジェクト)

MyApp  # 実験用の行列計算スクリプト (結果はSlackにポスト)
 ├─ pandas                # 直接依存
 │   ├── numpy              # 推移的依存
 │   └── python-dateutil    # 推移的依存
 │        └── six           # 推移的依存
 └─ requests              # 直接依存
     ├── certifi            # 推移的依存
     ├── charset-normalizer # 推移的依存

SW開発や利用における古典的な問題

依存関係の種類

直接依存と推移的依存

直接依存:MyApp → pandas
推移的依存:MyApp → pandas → numpy

各SWのリリース版は依存SWを内包していない
依存を宣言しているだけ

推移的依存を再帰的に手繰らないといけない

バージョン情報も解決すべき制約の一種

MyApp → pandas(>=3.0) (pandasのv3.0以上を利用する)

verが変わると振る舞いが変わる可能性がある
SWは常に後方互換を保っているとは限らない

Pythonの依存管理の例

依存関係の宣言

requirements.txt に依存を書き込む

$ mkdir MyApp && cd MyApp
$ vi requirements.txt  # 直接依存の宣言
pandas>=3.0   # v3.0以上ならOK
requests      # verはなんでもOK (自動的に最新を取得)

依存関係を解決

Pythonの標準パッケージマネージャpipを使う

$ pip install -r requirements.txt
Collecting pandas>=3.0 (from -r requirements.txt (line 1))
  Downloading pandas-3.0.3-...whl.metadata (79 kB)
Collecting requests    (from -r requirements.txt (line 2))
...

解決した依存関係を表示

$ pipdeptree  # 依存関係を表示
pandas==3.0.3
├── numpy            [required: >=1.26.0, installed: 2.5.0]
├── python-dateutil  [required: >=2.8.2,  installed: 2.9.0]
│   └── six          [required: >=1.5,    installed: 1.17.0]
requests==2.34.2
├── certifi          [required: >=2023.5.7, installed: ...]

依存関係の解決とは?

要求を満たす最新verを自動特定する作業のこと
各言語のSWリポジトリから自動取得される

一種の充足可能問題 (satisfiability problem, SAT)
充足できない場合もある

依存の衝突

要求する依存に矛盾がある
つまり充足可能問題を解けない状況のこと

衝突するように依存を変更

$ vi requirements.txt
pandas>=3.0
requests
python-dateutil==2.6  # 2.6への直接依存を追加 (pandas>=3.0と矛盾)

(再掲)

$ pipdeptree  # 依存関係を表示
pandas==3.0.3
├── numpy            [required: >=1.26.0, installed: 2.5.0]
├── python-dateutil  [required: >=2.8.2,  installed: 2.9.0]
                                 # ↑こことの矛盾

衝突の確認

$ pip install -r requirements.txt # 再インストール
...
ERROR: Cannot install -r requirements.txt (line 1) and
python-dateutil==2.6 because these package versions have
conflicting dependencies.

The conflict is caused by:
    The user requested python-dateutil==2.6
    pandas 3.0.3 depends on python-dateutil>=2.8.2
    pandas 3.0.2 depends on python-dateutil>=2.8.2
    pandas 3.0.1 depends on python-dateutil>=2.8.2
    pandas 3.0.0 depends on python-dateutil>=2.8.2

どの依存 (要求) と衝突するかも表示してくれる

SWは機能的豊富さだけでなく依存の柔軟さも重要

  • 制約の強いver依存を書かないべき
  • 極力新しいverに依存すべき

Pythonのパッケージ管理事情

Pythonのビルドツール

Pythonではあまりビルド工程が意識されない
Python処理系がうまく隠してくれるため

PythonとPM (パッケージマネージャ)

一方でPMには強い関心が寄せられる
Python処理系がうまくないため

PythonのPM周りはかなりカオス

~2008:easy_install
~2012:pip + virtualenv
~2018:pip + venv
~2023:Poetry / Pipenv / PDM / uv

Python処理系の何が悪いのか

Pythonはグローバルインストールが前提

例えばPythonプログラムのimport宣言に対して;

import pandas

Pythonはグローバルな場所を検索する

  • /usr/lib/python/site-package

pipのインストール先も当然グローバルな場所

他の言語はローカルインストールが多い

JavaやRustはカレントフォルダを検索する

  • ./ が優先,なければグローバルを検索

フォルダごとにインストール結果を分離できる

グローバルインストールでは分離ができない

複数プロジェクトのインストール結果が
同じフォルダに統合されてしまう

プロジェクト毎にインストール結果を分離したい
=️ ローカルインストールしたい

仮想環境の登場 (virtualenv / venv)

ローカルインストールのためのトリック

仮想環境を立ち上げると環境変数が書き換わる
➡️ グローバルな場所をローカルに差し替える
➡️ ローカルインストールできる

仮想環境と聞くと大げさに見えるが実際には
$PATH書き換えだけ

仮想環境を実際に作ってみる

$ cd ~/MyApp
$ python -m venv .venv  # .venvフォルダに仮想環境を作る
$ source .venv/bin/activate  # 仮想環境を実行 (env書き換え)
$ pip install -r requirements.txt # ローカルインストール
$ python main.py  # 実行

activateスクリプトの中身を覗いてみる

$ less .venv/bin/activate
...
export VIRTUAL_ENV=/home/shin/MyApp/.venv
...
_OLD_VIRTUAL_PATH="$PATH"       # PATHをバックアップ
PATH="$VIRTUAL_ENV/"bin":$PATH" # PATHを書き換え

結局$PATHを書き換えているだけ

$ echo $PATH
/home/shin/MyApp/.venv/bin:/usr/local/sbin:/usr/bin:...

pipのインストール結果はローカルに設置される

$ ls .venv/lib/python3.14/site-packages/
pandas
pandas-3.0.3.dist-info
requests
requests-2.34.2.dist-info

Pythonの実行バイナリもローカルにある

$ which python
/home/shin/MyApp/.venv/bin/python
$ which pip
/home/shin/MyApp/.venv/bin/pip

MyAppの実行に必要な全てのデータが
MyAppフォルダ内に閉じ込められている

仮想環境の終了

$ deactivate  # 書き換えた$PATHを戻す

もっと楽にできないか?

仮想環境の立ち上げがくどい

(再掲)

$ cd ~/MyApp
$ python -m venv .venv  # .venvフォルダに仮想環境を作る
$ source .venv/bin/activate  # 仮想環境を実行 (env書き換え)
$ pip install -r requirements.txt # ローカルインストール
$ python main.py  # 実行

毎回やる作業は $ source .venv/bin/activate の部分

ここを隠す方法はないか?
仮想環境を意識しなくてもよくできないか?

Poetry / Pipenv / PDM / uvの登場

直感的には pip + venv をまとめた仕組み

uvの使い方

初期設定

$ cd MyApp
$ uv init                 # プロジェクトの初期化
$ uv add <package>        # pip installと同じ意味
$ uv run python main.py   # python main.pyと同じ意味
$ ls -a1
.git             # gitも生成されている
.gitignore       # git関係
.python-version
.venv            # 仮想環境
main.py
pyproject.toml   # プロジェクト全体の設定 (依存関係を含む)
README.md
uv.lock

裏側ではvenvが動いているが利用者は意識しない

演習✍️️ (10m)

Pythonのパッケージマネージャuvを試せ

※ 各ステップのコマンドはLinuxでの作業方法の一例である
  実行すべきコマンドは自身の環境にあわせて適宜読み替えること

提出方法

作業1~7の ??? の部分 (出力結果) を以下書式の
テキストにまとめ,CLEに提出すること

# 1
.  ..  .git  .gitignore  main.py  pyproject.toml  ...

# 2
...

0. 前準備

$ cd <work>     # 適当な空の作業フォルダへ移動
$ uv init       # プロジェクトの初期化

1. 生成されたファイル一覧を確認せよ

$ ls -a  # 隠しファイルも表示
???

2. uvの設定ファイルを確認せよ

$ cat pyproject.toml  # pyproject.tomlの中身を表示
???

3. 2つの依存関係を宣言せよ

$ uv add 'pandas>=3.0'
???
$ uv add requests
???

4. 再度pyproject.tomlの中身を確認せよ

$ cat pyproject.toml  # pyproject.tomlの中身を表示
???

5. 解決された依存一覧を確認せよ

$ uv tree
???

6. 依存libのインストール先を確認せよ

$ ls .venv/lib/python*/site-packages
???
$ # ls .venv/lib/site-packages (上記で失敗する場合はこちら)

7. 依存の衝突を試せ

$ uv add python-dateutil==2.6
???

衝突の詳細は依存の衝突 (#23)を参照せよ

モダンSW開発 -ビルド-


目次

・ビルド?建築?
・ビルドツール
・Gradleを試す

・依存関係
・依存の衝突
・Pythonのパッケージ管理事情
・演習✍️️

・ビルドツールの歴史
・ビルドツールと版管理
"X as a Code"
・SW開発外でのパッケージ管理

ビルドツールの歴史

make (1977~)

命令的 / ビルドツールの始祖

Makefile

CC=gcc
CFLAGS=-O3
SOURCES=main.c functions.c

all: $(SOURCES)

%.o: %.c
    $(CC) $(CFLAGS) $< -o $@

clean:
    rm -rf *.o

コンパイル処理の手順を記述

Ant (2000~)

命令的 / Java用

build.xml

<project name="HelloWorld" basedir="." default="main">
    <property name="src.dir"     value="src"/>
    <property name="build.dir"   value="build"/>
    <property name="classes.dir" value="${build.dir}/classes"/>

    <target name="clean">
        <delete dir="${build.dir}"/>
    </target>

    <target name="compile">
        <mkdir dir="${classes.dir}"/>
        <javac srcdir="${src.dir}" destdir="${classes.dir}"/>
    </target>
    ...

書式がXMLになっただけで本質はmakeと同じ

Maven (2004~) / Gradle (2008~)

宣言的 (命令を列挙しない)

build.gradle

plugins {
    application
}
dependencies {
    implementation 'commons-lang3:3.17.0'
    testImplementation 'junit-jupiter:5.13.1'
}
application {
    mainClass = "org.example.App"
}
...

手続きではなく規約を書く (設定ではない)
CoCパラダイムに従う

CoC

Convention over configuration

「設定より規約」というパラダイム

開発者は規約に従わない部分だけを宣言する
規約≒デフォルト設定と考えると分かりやすい

Gradleの規約の一例

プロダクトコードは src/main/java の配下へ
テストコードは src/test/java の配下へ
ビルド結果は build/classes/java へ出力

規約化できない要素 (設定が必要な事項)

プロジェクト名 / 依存関係 / mainクラスの名前 ...

ビルドツールと版管理

ソース/テストだけでなくビルド方法も版管理へ

uvの場合はpyproject.tomlのこと
共同開発で特に強力
ビルドの再現性の確保

ビルド方法の進化も版の一種

ビルドスクリプトを起因としたバグもある
版管理しておけばロールバックも可能

ビルドスクリプトの同時編集にも耐えられる

CI/CDとの相性の良さ

サーバ上での自動ビルドが可能
ビルド方法を機械処理可能とするうまみ

"X as a code"

ある対象作業Xをコード化する考え

コード化=自動化可

一種のエンジニアリング

作業Xの属人性を排除できる
再現性も高い

Xの一例

Infrastructure (IaC)
Configuration (CaC)
Requirements (RaC)
Security (SaC)
テストもコード化できる

コード化 ≒ バグとの戦い

雑談

LaTeXのビルドツール

雑談

LaTeXのコンパイルは大変

$ platex x    # tex       → aux(壊れ) + dvi(壊れ)
$ bibtex x    # bib + aux → bbl
$ platex x    # tex + bbl → aux       + dvi(壊れ)
$ platex x    # tex + bbl → aux       + dvi
$ dvipdfmx x  # dvi       → pdf

Latexmk

$ latexmk x   # これだけでOK
$ cat .latexmkrc  # 設定ファイルは必要 (Antに近い)
$latex = 'platex -synctex=1 %O %S';
$bibtex = 'bibtexu %O %B';
$dvipdf = 'dvipdfmx %O -o %D %S';
...

SW開発外でのパッケージ管理

SW開発外でも広く使われる

インストール作業のコード化が可能
新規PC立ち上げ時に強力

  • ダウンロード + 依存関係解決 + ビルド + 設置

Linux / BSD系

apt / yum / dpkg

Mac

Homebrew

Windows

Chocolatey

モダンSW開発 -ビルド-


目次

・ビルド?建築?
・ビルドツール
・Gradleを試す

・依存関係
・依存の衝突
・Pythonのパッケージ管理事情
・演習✍️️

・ビルドツールの歴史
・ビルドツールと版管理
"X as a Code"
・SW開発外でのパッケージ管理

ビルドツールまとめ

ビルドツールを積極的に使おう

自動化の恩恵
版管理・CI/CDとの相性が良い

規約のうまみ
規約は一種の共通認識ともいえる

依存関係

必ずパッケージマネージャを使おう
既存libを積極的に使おう
バグを減らす一つの方法はコードを書かないこと

柔軟な依存を宣言しよう
強い制約はSWの可搬性を損なう

%%{init:{'theme':'base','themeVariables':{'primaryColor':'#6A7FAB','primaryTextColor':'#FAFBF9','primaryBorderColor':'#6A7FAB','lineColor':'#6A7FABCC','textColor':'#6A7FABCC','fontSize':'20px','padding':0}}}%% %%{ init: { 'flowchart': { 'nodeSpacing': 10, 'rankSpacing': 100 } } }%%

![](https://docs.gradle.org/current/userguide/img/task-dag-examples.png) <subb>https://docs.gradle.org/current/userguide/build_lifecycle.html</subb>

--- # headless <div class="corner-triangle"><div class="corner-triangle-text">雑談</div></div> ## SW開発の基本テクニック GUIを持たない状態で起動するモードのこと 大きなシステム開発でよく採用されている ```shell $ chrome --headless # GUIなしでChromeが立ち上がる ``` ## as サーバモード サーバとして起動する場合はGUIがいらない ## for テストの軽量化 GUIに無関係なテストはheadlessモードでテスト ## for 自動操作 GUIではなくCUI経由で対象SWを動かしたい

ビルドツールに規約が存在する =規約に従うプロジェクトを自動で生成できる

規約に従うなら何も書かなくて良い

--- # PyBuilderを試す ## pyプロジェクトの生成 ```shell $ mkdir py-trial; cd py-trial $ pyb --start-project Project name (default: 'py-trial') : Source directory (default: 'src/main/python') : Docs directory (default: 'docs') : Unittest directory (default: 'src/unittest/python') : Scripts directory (default: 'src/main/scripts') : Use plugin python.flake8 (Y/n)? (default: 'y') : Use plugin python.coverage (Y/n)? (default: 'y') : ... ``` ## 簡単なソースコードを追加 ```shell $ echo 'print("hi")' > src/main/python/main.py ``` --- ## ビルド ```shell $ pyb ... Build finished at 2025-05-05 13:42:14 Build took 4 seconds (4787 ms) ``` ## ビルド結果の確認 ```shell $ tree target/dist target/dist └── py-trial-1.0.dev0 ├── main.py ├── scripts └── setup.py ``` PyBuilderの利用プロジェクトは多くはない おすすめというわけではない Pythonではデファクトがまだなさそう 選択肢が多い (根本的にpyはビルドが楽)

--- # バージョンロック ## 宣言した依存が常に正とは限らない `pandas>=2.0`という依存を宣言して開発を続ける いつのまにか`p --- 依存解決自体は標準でpip gradle 作法の理解にも繋がる `src/main/java` ? 版管理との組み合わせ ソース テスト ビルド方法 自己完結 自動化が可能