This PR does two things:

1. I moved `package.json` into the root directory. Why? We are using `npx` from the `Makefile` which is executing usually under the root directory. If we install `node_modules` in `src/js` then these invocations don't see `node_modules` and install an extra copy of the packages. So we want `node_modules` in the root directory. node wants to put `node_modules` in the same directory as `package.json` hence I moved `package.json`. I also updated the build rules so that any recipe that uses `npx` has a dependency on `node_modules/.installed`.
2. We invoke `file_packager.py` from both the Makefile (in two places) and from `build_pkg`. There has been some drift in which options are applied in the two places. I extracted the common options and the task of locating the `file_packager.py` script into `tools/file_packager.sh`. I also shortened the make rules for `distutils.data` and `test.data` and made some minor updates to the dev docs about `file_packager.py`.
MAINT Improvements to node and file_packager invocations. This PR does two things:

1. I moved `package.json` into the root directory. Why? We are using `npx` from the `Makefile` which is executing usually under the root directory. If we install `node_modules` in `src/js` then these invocations don't see `node_modules` and install an extra copy of the packages. So we want `node_modules` in the root directory. node wants to put `node_modules` in the same directory as `package.json` hence I moved `package.json`. I also updated the build rules so that any recipe that uses `npx` has a dependency on `node_modules/.installed`.
2. We invoke `file_packager.py` from both the Makefile (in two places) and from `build_pkg`. There has been some drift in which options are applied in the two places. I extracted the common options and the task of locating the `file_packager.py` script into `tools/file_packager.sh`. I also shortened the make rules for `distutils.data` and `test.data` and made some minor updates to the dev docs about `file_packager.py`.
from pathlib import Path
import sys
import argparse
from dataclasses import dataclass

import pytest

sys.path.append(str(Path(__file__).parents[2]))

from pyodide_build.pywasmcross import handle_command  # noqa: E402
from pyodide_build.pywasmcross import f2c  # noqa: E402
from pyodide_build.pywasmcross import make_parser


@dataclass
class BuildArgs:
    """An object to hold build arguments"""

    cflags: str = ""
    cxxflags: str = ""
    ldflags: str = ""
    host: str = ""
    replace_libs: str = ""
    install_dir: str = ""


def _args_wrapper(func):
    """Convert function to take as input / return a string instead of a
    list of arguments

    Also sets dryrun=True
    """

    def _inner(line, *pargs):
        args = line.split()
        res = func(args, *pargs, dryrun=True)
        if hasattr(res, "__len__"):
            return " ".join(res)
        else:
            return res

    return _inner


handle_command_wrap = _args_wrapper(handle_command)
f2c_wrap = _args_wrapper(f2c)


def test_handle_command():
    args = BuildArgs()
    assert handle_command_wrap("gcc -print-multiarch", args) is None
    assert handle_command_wrap("gcc test.c", args) == "emcc test.c"
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -c test.o -o test.so"
    )

    # check cxxflags injection and cpp detection
    args = BuildArgs(
        cflags="-I./lib2",
        cxxflags="-std=c++11",
        ldflags="-lm",
    )
    assert (
        handle_command_wrap("gcc -I./lib1 test.cpp -o test.o", args)
        == "em++ -I./lib2 -std=c++11 -I./lib1 test.cpp -o test.o"
    )

    # check ldflags injection
    args = BuildArgs(
        cflags="", cxxflags="", ldflags="-lm", host="", replace_libs="", install_dir=""
    )
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -lm -c test.o -o test.so"
    )

    # check library replacement and removal of double libraries
    args = BuildArgs(
        replace_libs="bob=fred",
    )
    assert (
        handle_command_wrap("gcc -shared test.o -lbob -ljim -ljim -o test.so", args)
        == "emcc test.o -lfred -ljim -o test.so"
    )

    # compilation checks in numpy
    assert handle_command_wrap("gcc /usr/file.c", args) is None


@pytest.mark.parametrize(
    "in_ext, out_ext, executable, flag_name",
    [
        (".c", ".o", "emcc", "cflags"),
        (".cpp", ".o", "em++", "cxxflags"),
        (".c", ".so", "emcc", "ldflags"),
    ],
)
def test_handle_command_optflags(in_ext, out_ext, executable, flag_name):
    # Make sure that when multiple optflags are present those in cflags,
    # cxxflags, or ldflags has priority

    args = BuildArgs(**{flag_name: "-Oz"})
    assert (
        handle_command_wrap(f"gcc -O3 test.{in_ext} -o test.{out_ext}", args)
        == f"{executable} -Oz test.{in_ext} -o test.{out_ext}"
    )


def test_f2c():
    assert f2c_wrap("gfortran test.f") == "gfortran test.c"
    assert f2c_wrap("gcc test.c") is None
    assert f2c_wrap("gfortran --version") is None
    assert (
        f2c_wrap("gfortran --shared -c test.o -o test.so")
        == "gfortran --shared -c test.o -o test.so"
    )


def test_conda_compiler_compat():
    args = BuildArgs()
    assert handle_command_wrap(
        "gcc -shared -c test.o -B /compiler_compat -o test.so", args
    ) == ("emcc -c test.o -o test.so")


def test_environment_var_substitution(monkeypatch):
    monkeypatch.setenv("PYODIDE_BASE", "pyodide_build_dir")
    monkeypatch.setenv("BOB", "Robert Mc Roberts")
    monkeypatch.setenv("FRED", "Frederick F. Freddertson Esq.")
    monkeypatch.setenv("JIM", "James Ignatius Morrison:Jimmy")
    call_args = 'pywasmcross.py --ldflags "-l$(PYODIDE_BASE)" --cxxflags $(BOB) --cflags $(FRED) --replace-libs $(JIM)'
    monkeypatch.setattr(sys, "argv", call_args.split(" "))
    parser = argparse.ArgumentParser()
    make_parser(parser)
    args = parser.parse_args()
    assert (
        args.cflags == "Frederick F. Freddertson Esq."
        and args.cxxflags == "Robert Mc Roberts"
        and args.ldflags == '"-lpyodide_build_dir"'
        and args.replace_libs == "James Ignatius Morrison:Jimmy"
    )

from pathlib import Path
import sys
import argparse
from dataclasses import dataclass

import pytest

sys.path.append(str(Path(__file__).parents[2]))

from pyodide_build.pywasmcross import handle_command  # noqa: E402
from pyodide_build.pywasmcross import f2c  # noqa: E402
from pyodide_build.pywasmcross import make_parser


@dataclass
class BuildArgs:
    """An object to hold build arguments"""

    cflags: str = ""
    cxxflags: str = ""
    ldflags: str = ""
    host: str = ""
    replace_libs: str = ""
    install_dir: str = ""


def _args_wrapper(func):
    """Convert function to take as input / return a string instead of a
    list of arguments

    Also sets dryrun=True
    """

    def _inner(line, *pargs):
        args = line.split()
        res = func(args, *pargs, dryrun=True)
        if hasattr(res, "__len__"):
            return " ".join(res)
        else:
            return res

    return _inner


handle_command_wrap = _args_wrapper(handle_command)
f2c_wrap = _args_wrapper(f2c)


def test_handle_command():
    args = BuildArgs()
    assert handle_command_wrap("gcc -print-multiarch", args) is None
    assert handle_command_wrap("gcc test.c", args) == "emcc test.c"
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -c test.o -o test.so"
    )

    # check cxxflags injection and cpp detection
    args = BuildArgs(
        cflags="-I./lib2",
        cxxflags="-std=c++11",
        ldflags="-lm",
    )
    assert (
        handle_command_wrap("gcc -I./lib1 test.cpp -o test.o", args)
        == "em++ -I./lib2 -std=c++11 -I./lib1 test.cpp -o test.o"
    )

    # check ldflags injection
    args = BuildArgs(
        cflags="", cxxflags="", ldflags="-lm", host="", replace_libs="", install_dir=""
    )
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -lm -c test.o -o test.so"
    )

    # check library replacement and removal of double libraries
    args = BuildArgs(
        replace_libs="bob=fred",
    )
    assert (
        handle_command_wrap("gcc -shared test.o -lbob -ljim -ljim -o test.so", args)
        == "emcc test.o -lfred -ljim -o test.so"
    )

    # compilation checks in numpy
    assert handle_command_wrap("gcc /usr/file.c", args) is None


@pytest.mark.parametrize(
    "in_ext, out_ext, executable, flag_name",
    [
        (".c", ".o", "emcc", "cflags"),
        (".cpp", ".o", "em++", "cxxflags"),
        (".c", ".so", "emcc", "ldflags"),
    ],
)
def test_handle_command_optflags(in_ext, out_ext, executable, flag_name):
    # Make sure that when multiple optflags are present those in cflags,
    # cxxflags, or ldflags has priority

    args = BuildArgs(**{flag_name: "-Oz"})
    assert (
        handle_command_wrap(f"gcc -O3 test.{in_ext} -o test.{out_ext}", args)
        == f"{executable} -Oz test.{in_ext} -o test.{out_ext}"
    )


def test_f2c():
    assert f2c_wrap("gfortran test.f") == "gfortran test.c"
    assert f2c_wrap("gcc test.c") is None
    assert f2c_wrap("gfortran --version") is None
    assert (
        f2c_wrap("gfortran --shared -c test.o -o test.so")
        == "gfortran --shared -c test.o -o test.so"
    )


def test_conda_compiler_compat():
    args = BuildArgs()
    assert handle_command_wrap(
        "gcc -shared -c test.o -B /compiler_compat -o test.so", args
    ) == ("emcc -c test.o -o test.so")


def test_environment_var_substitution(monkeypatch):
    monkeypatch.setenv("PYODIDE_BASE", "pyodide_build_dir")
    monkeypatch.setenv("BOB", "Robert Mc Roberts")
    monkeypatch.setenv("FRED", "Frederick F. Freddertson Esq.")
    monkeypatch.setenv("JIM", "James Ignatius Morrison:Jimmy")
    call_args = 'pywasmcross.py --ldflags "-l$(PYODIDE_BASE)" --cxxflags $(BOB) --cflags $(FRED) --replace-libs $(JIM)'
    monkeypatch.setattr(sys, "argv", call_args.split(" "))
    parser = argparse.ArgumentParser()
    make_parser(parser)
    args = parser.parse_args()
    assert (
        args.cflags == "Frederick F. Freddertson Esq."
        and args.cxxflags == "Robert Mc Roberts"
        and args.ldflags == '"-lpyodide_build_dir"'
        and args.replace_libs == "James Ignatius Morrison:Jimmy"
    )
