Skip to content

Commit

Permalink
Merge pull request #236 from numinit/next
Browse files Browse the repository at this point in the history
Package as a Nix flake to fix problems with dependencies
  • Loading branch information
sandreas authored Apr 18, 2023
2 parents b9844fd + 59e0a88 commit ccfb548
Show file tree
Hide file tree
Showing 13 changed files with 1,085 additions and 6 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,20 @@ Special thanks to all sponsors donating a monthly amount of `>= 25.00$`.
<a href="https://github.com/sponsors/sandreas"><img src="./assets/help.svg" width="200" alt="sponsor me and donate" style="margin:auto;"></a>
</p>

## Usage with Nix

Get [Nix](https://nixos.org/download.html) and ensure that [Flakes](https://nixos.wiki/wiki/Flakes#Permanent) are enabled.

- Running: `nix run github:sandreas/m4b-tool` or `nix run github:sandreas/m4b-tool#m4b-tool-libfdk`
- The latter will build FFMpeg using libfdk_aac, which will take longer.
- Building: `nix build github:sandreas/m4b-tool` or `nix build github:sandreas/m4b-tool#m4b-tool-libfdk`
- Wrapper script is located at `./result/bin/m4b-tool`
- Developing: Clone and `nix develop`
- When done updating dependencies, run `composer2nix --executable --composition=composer.nix` to update the .nix files

## Announcement

A few months ago I noticed, that using `PHP` as programming language is a limiting factor for `m4b-tool` and its further development. Multiple dependencies (`php`, `ffmpeg`, `mp4v2`, `fdkaac` and others) make it quite hard to improve the user experience. So I started an experiment, that now has reached an early alpha level and can be tried out. The command line tool is written in `C#`, fully open source and is called `tone`. It already has a pretty decent feature set, so if you would like to try it, here it is:
I started an experiment, that now has reached an early alpha level and can be tried out. The command line tool is written in `C#`, fully open source and is called `tone`. It already has a pretty decent feature set, so if you would like to try it, here it is:

https://github.com/sandreas/tone

Expand Down
244 changes: 244 additions & 0 deletions composer-env.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# This file originates from composer2nix

{ stdenv, lib, writeTextFile, fetchurl, php, unzip, phpPackages }:

let
inherit (phpPackages) composer;

filterSrc = src:
builtins.filterSource (path: type: type != "directory" || (baseNameOf path != ".git" && baseNameOf path != ".git" && baseNameOf path != ".svn")) src;

buildZipPackage = { name, src }:
stdenv.mkDerivation {
inherit name src;
nativeBuildInputs = [ unzip ];
buildCommand = ''
shopt -s dotglob
unzip $src
baseDir=$(find . -type d -mindepth 1 -maxdepth 1)
cd $baseDir
mkdir -p $out
mv * $out
'';
};

buildPackage =
{ name
, src
, packages ? {}
, devPackages ? {}
, buildInputs ? []
, symlinkDependencies ? false
, executable ? false
, removeComposerArtifacts ? false
, postInstall ? ""
, noDev ? false
, composerExtraArgs ? ""
, unpackPhase ? "true"
, buildPhase ? "true"
, ...}@args:

let
reconstructInstalled = writeTextFile {
name = "reconstructinstalled.php";
executable = true;
text = ''
#! ${php}/bin/php
<?php
if(file_exists($argv[1]))
{
$composerLockStr = file_get_contents($argv[1]);
if($composerLockStr === false)
{
fwrite(STDERR, "Cannot open composer.lock contents\n");
exit(1);
}
else
{
$config = json_decode($composerLockStr, true);
if(array_key_exists("packages", $config))
$allPackages = $config["packages"];
else
$allPackages = array();
${lib.optionalString (!noDev) ''
if(array_key_exists("packages-dev", $config))
$allPackages = array_merge($allPackages, $config["packages-dev"]);
''}
$packagesStr = json_encode($allPackages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
print($packagesStr);
}
}
else
print("[]");
?>
'';
};

constructBin = writeTextFile {
name = "constructbin.php";
executable = true;
text = ''
#! ${php}/bin/php
<?php
$composerJSONStr = file_get_contents($argv[1]);
if($composerJSONStr === false)
{
fwrite(STDERR, "Cannot open composer.json contents\n");
exit(1);
}
else
{
$config = json_decode($composerJSONStr, true);
if(array_key_exists("bin-dir", $config))
$binDir = $config["bin-dir"];
else
$binDir = "bin";
if(array_key_exists("bin", $config))
{
if(!file_exists("vendor/".$binDir))
mkdir("vendor/".$binDir);
foreach($config["bin"] as $bin)
symlink("../../".$bin, "vendor/".$binDir."/".basename($bin));
}
}
?>
'';
};

bundleDependencies = dependencies:
lib.concatMapStrings (dependencyName:
let
dependency = dependencies.${dependencyName};
in
''
${if dependency.targetDir == "" then ''
vendorDir="$(dirname ${dependencyName})"
mkdir -p "$vendorDir"
${if symlinkDependencies then
''ln -s "${dependency.src}" "$vendorDir/$(basename "${dependencyName}")"''
else
''cp -av "${dependency.src}" "$vendorDir/$(basename "${dependencyName}")"''
}
'' else ''
namespaceDir="${dependencyName}/$(dirname "${dependency.targetDir}")"
mkdir -p "$namespaceDir"
${if symlinkDependencies then
''ln -s "${dependency.src}" "$namespaceDir/$(basename "${dependency.targetDir}")"''
else
''cp -av "${dependency.src}" "$namespaceDir/$(basename "${dependency.targetDir}")"''
}
''}
'') (builtins.attrNames dependencies);

extraArgs = removeAttrs args [ "packages" "devPackages" "buildInputs" ];
in
stdenv.mkDerivation ({
buildInputs = [ php composer ] ++ buildInputs;

inherit unpackPhase buildPhase;

installPhase = ''
${if executable then ''
mkdir -p $out/share/php
cp -av $src $out/share/php/$name
chmod -R u+w $out/share/php/$name
cd $out/share/php/$name
'' else ''
cp -av $src $out
chmod -R u+w $out
cd $out
''}
# Remove unwanted files
rm -f *.nix
export HOME=$TMPDIR
# Remove the provided vendor folder if it exists
rm -Rf vendor
# If there is no composer.lock file, compose a dummy file.
# Otherwise, composer attempts to download the package.json file from
# the registry which we do not want.
if [ ! -f composer.lock ]
then
cat > composer.lock <<EOF
{
"packages": []
}
EOF
fi
# Reconstruct the installed.json file from the lock file
mkdir -p vendor/composer
${php}/bin/php ${reconstructInstalled} composer.lock > vendor/composer/installed.json
# Copy or symlink the provided dependencies
cd vendor
${bundleDependencies packages}
${lib.optionalString (!noDev) (bundleDependencies devPackages)}
cd ..
# Reconstruct autoload scripts
# We use the optimize feature because Nix packages cannot change after they have been built
# Using the dynamic loader for a Nix package is useless since there is nothing to dynamically reload.
composer dump-autoload --optimize ${lib.optionalString noDev "--no-dev"} ${composerExtraArgs}
# Run the install step as a validation to confirm that everything works out as expected
composer install --optimize-autoloader ${lib.optionalString noDev "--no-dev"} ${composerExtraArgs}
${lib.optionalString executable ''
# Reconstruct the bin/ folder if we deploy an executable project
${php}/bin/php ${constructBin} composer.json
ln -s $(pwd)/vendor/bin $out/bin
''}
${lib.optionalString (!symlinkDependencies) ''
# Patch the shebangs if possible
if [ -d $(pwd)/vendor/bin ]
then
# Look for all executables in bin/
for i in $(pwd)/vendor/bin/*
do
# Look for their location
realFile=$(readlink -f "$i")
# Restore write permissions
chmod u+wx "$(dirname "$realFile")"
chmod u+w "$realFile"
# Patch shebang
sed -e "s|#!/usr/bin/php|#!${php}/bin/php|" \
-e "s|#!/usr/bin/env php|#!${php}/bin/php|" \
"$realFile" > tmp
mv tmp "$realFile"
chmod u+x "$realFile"
done
fi
''}
if [ "$removeComposerArtifacts" = "1" ]
then
# Remove composer stuff
rm -f composer.json composer.lock
fi
# Execute post install hook
runHook postInstall
'';
} // extraArgs);
in
{
inherit filterSrc;
composer = lib.makeOverridable composer;
buildZipPackage = lib.makeOverridable buildZipPackage;
buildPackage = lib.makeOverridable buildPackage;
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "sandreas/m4b-tool",
"minimum-stability": "dev",
"prefer-stable": true,
"platform":{"php":"7.4"},
Expand Down
14 changes: 14 additions & 0 deletions composer.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{pkgs ? import <nixpkgs> {
inherit system;
}, system ? builtins.currentSystem, noDev ? false, php ? pkgs.php, phpPackages ? pkgs.phpPackages}:

let
composerEnv = import ./composer-env.nix {
inherit (pkgs) stdenv lib writeTextFile fetchurl unzip;
inherit php phpPackages;
};
in
import ./php-packages.nix {
inherit composerEnv noDev;
inherit (pkgs) fetchurl fetchgit fetchhg fetchsvn;
}
104 changes: 104 additions & 0 deletions default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{ pkgs, lib, stdenv, fetchFromGitHub, fetchurl
, runtimeShell
, php82, php82Packages
, ffmpeg_5-headless, mp4v2, fdk_aac, fdk-aac-encoder
, useLibfdkFfmpeg ? false
}:

let
m4bToolPhp = php82.buildEnv {
extensions = ({ enabled, all }: enabled ++ (with all; [
dom mbstring tokenizer xmlwriter openssl
]));

extraConfig = ''
date.timezone = UTC
error_reporting = E_ALL & ~E_STRICT & ~E_NOTICE & ~E_DEPRECATED
'';
};

m4bToolPhpPackages = php82Packages;

m4bToolComposer = pkgs.callPackage ./composer.nix {
php = m4bToolPhp;
phpPackages = m4bToolPhpPackages;
};

m4bToolFfmpeg = if useLibfdkFfmpeg then ffmpeg_5-headless.overrideAttrs (prev: rec {
configureFlags = prev.configureFlags ++ [
"--enable-libfdk-aac"
"--enable-nonfree"
];
buildInputs = prev.buildInputs ++ [
fdk_aac
];
}) else ffmpeg_5-headless;
in
m4bToolComposer.overrideAttrs (prev: rec {
pname = "m4b-tool";
version = "0.5";

buildInputs = [
m4bToolPhp m4bToolFfmpeg mp4v2 fdk-aac-encoder
];

nativeBuildInputs = [
m4bToolPhp m4bToolPhpPackages.composer
];

postInstall = ''
# Fix the version
sed -i 's!@package_version@!${version}!g' bin/m4b-tool.php
'';

postFixup = ''
# Wrap it
rm -rf $out/bin
mkdir -p $out/bin
# makeWrapper fails for this on macOS
cat >$out/bin/m4b-tool <<EOF
#!${runtimeShell}
export PATH=${lib.makeBinPath buildInputs}
export M4B_TOOL_DISABLE_TONE=true
exec ${m4bToolPhp}/bin/php $out/share/php/sandreas-m4b-tool/bin/m4b-tool.php "\$@"
EOF
chmod +x $out/bin/m4b-tool
'';

doInstallCheck = true;

installCheckPhase = let
exampleAudiobook = fetchurl {
name = "audiobook";
url = "https://archive.org/download/M4bCollectionOfLibrivoxAudiobooks/ArtOfWar-64kb.m4b";
sha256 = "00cvbk2a4iyswfmsblx2h9fcww2mvb4vnlf22gqgi1ldkw67b5w7";
};
in ''
# Run the unit test suite
php vendor/bin/phpunit tests
# Check that the audiobook split actually works
(
mkdir -p audiobook
cd audiobook
cp ${exampleAudiobook} audiobook.m4b
$out/bin/m4b-tool split -vvv -o . audiobook.m4b
if ! grep -q 'The Nine Situations' audiobook.chapters.txt; then
exit 1
fi
if [ ! -f '006-11 The Nine Situations.m4b' ]; then
exit 1
fi
)
rm -rf audiobook
'';

passthru = {
dependencies = buildInputs;
devDependencies = nativeBuildInputs;
};
})
Loading

0 comments on commit ccfb548

Please sign in to comment.