Our package build process currently has a significant flaw: we first run `setup.py`, recording all compilation commands, then we rewrite these compilation commands to invoke `emcc` and replay them, and then we pray that the cross compiled executables ended up in the right place to go into the wheel. This is not a good strategy because the build script is allowed to implement arbitrary logic, and if it moves, renames, etc any of the output files then we lose track of them. This has repeatedly caused difficulty for us.

However, we also make no particularly significant use of the two pass approach. We can just do the simpler thing: capture the compiler commands as they occur, modify them as needed, and then run the fixed command.

I also added a patch to fix the numpy feature detection for wasm so that we don't have to include `_npyconfig.h` and `config.h`, numpy can generate them in the way it would for a native build. I opened a numpy PR that would fix the detection for us upstream:
https://github.com/numpy/numpy/pull/21154

This clears the way for us to switch to using pypa/build (as @henryiii has suggested) by removing our dependence on specific setuptools behavior.

This is on top of #2238.
No replay. Our package build process currently has a significant flaw: we first run `setup.py`, recording all compilation commands, then we rewrite these compilation commands to invoke `emcc` and replay them, and then we pray that the cross compiled executables ended up in the right place to go into the wheel. This is not a good strategy because the build script is allowed to implement arbitrary logic, and if it moves, renames, etc any of the output files then we lose track of them. This has repeatedly caused difficulty for us.

However, we also make no particularly significant use of the two pass approach. We can just do the simpler thing: capture the compiler commands as they occur, modify them as needed, and then run the fixed command.

I also added a patch to fix the numpy feature detection for wasm so that we don't have to include `_npyconfig.h` and `config.h`, numpy can generate them in the way it would for a native build. I opened a numpy PR that would fix the detection for us upstream:
https://github.com/numpy/numpy/pull/21154

This clears the way for us to switch to using pypa/build (as @henryiii has suggested) by removing our dependence on specific setuptools behavior.

This is on top of #2238.
/**
 * The Emscripten Module.
 *
 * @private
 */
export let Module: any = {};
Module.noImageDecoding = true;
Module.noAudioDecoding = true;
Module.noWasmDecoding = false; // we preload wasm using the built in plugin now
Module.preloadedWasm = {};
Module.preRun = [];

export let API: any = {};
Module.API = API;
export let Hiwire: any = {};
Module.hiwire = Hiwire;

// Put things that are exposed only for testing purposes here.
export let Tests: any = {};
API.tests = Tests;

/**
 *
 * @param stdin
 * @param stdout
 * @param stderr
 * @private
 */
export function setStandardStreams(
  stdin?: () => string,
  stdout?: (a: string) => void,
  stderr?: (a: string) => void
) {
  // For stdout and stderr, emscripten provides convenient wrappers that save us the trouble of converting the bytes into a string
  if (stdout) {
    Module.print = stdout;
  }

  if (stderr) {
    Module.printErr = stderr;
  }

  // For stdin, we have to deal with the low level API ourselves
  if (stdin) {
    Module.preRun.push(function () {
      Module.FS.init(createStdinWrapper(stdin), null, null);
    });
  }
}

function createStdinWrapper(stdin: () => string) {
  // When called, it asks the user for one whole line of input (stdin)
  // Then, it passes the individual bytes of the input to emscripten, one after another.
  // And finally, it terminates it with null.
  const encoder = new TextEncoder();
  let input = new Uint8Array(0);
  let inputIndex = -1; // -1 means that we just returned null
  function stdinWrapper() {
    try {
      if (inputIndex === -1) {
        let text = stdin();
        if (text === undefined || text === null) {
          return null;
        }
        if (typeof text !== "string") {
          throw new TypeError(
            `Expected stdin to return string, null, or undefined, got type ${typeof text}.`
          );
        }
        if (!text.endsWith("\n")) {
          text += "\n";
        }
        input = encoder.encode(text);
        inputIndex = 0;
      }

      if (inputIndex < input.length) {
        let character = input[inputIndex];
        inputIndex++;
        return character;
      } else {
        inputIndex = -1;
        return null;
      }
    } catch (e) {
      // emscripten will catch this and set an IOError which is unhelpful for
      // debugging.
      console.error("Error thrown in stdin:");
      console.error(e);
      throw e;
    }
  }
  return stdinWrapper;
}

/**
 * Make the home directory inside the virtual file system,
 * then change the working directory to it.
 *
 * @param path
 * @private
 */
export function setHomeDirectory(path: string) {
  Module.preRun.push(function () {
    const fallbackPath = "/";
    try {
      Module.FS.mkdirTree(path);
    } catch (e) {
      console.error(`Error occurred while making a home directory '${path}':`);
      console.error(e);
      console.error(`Using '${fallbackPath}' for a home directory instead`);
      path = fallbackPath;
    }
    Module.ENV.HOME = path;
    Module.FS.chdir(path);
  });
}

/**
 * The Emscripten Module.
 *
 * @private
 */
export let Module: any = {};
Module.noImageDecoding = true;
Module.noAudioDecoding = true;
Module.noWasmDecoding = false; // we preload wasm using the built in plugin now
Module.preloadedWasm = {};
Module.preRun = [];

export let API: any = {};
Module.API = API;
export let Hiwire: any = {};
Module.hiwire = Hiwire;

// Put things that are exposed only for testing purposes here.
export let Tests: any = {};
API.tests = Tests;

/**
 *
 * @param stdin
 * @param stdout
 * @param stderr
 * @private
 */
export function setStandardStreams(
  stdin?: () => string,
  stdout?: (a: string) => void,
  stderr?: (a: string) => void
) {
  // For stdout and stderr, emscripten provides convenient wrappers that save us the trouble of converting the bytes into a string
  if (stdout) {
    Module.print = stdout;
  }

  if (stderr) {
    Module.printErr = stderr;
  }

  // For stdin, we have to deal with the low level API ourselves
  if (stdin) {
    Module.preRun.push(function () {
      Module.FS.init(createStdinWrapper(stdin), null, null);
    });
  }
}

function createStdinWrapper(stdin: () => string) {
  // When called, it asks the user for one whole line of input (stdin)
  // Then, it passes the individual bytes of the input to emscripten, one after another.
  // And finally, it terminates it with null.
  const encoder = new TextEncoder();
  let input = new Uint8Array(0);
  let inputIndex = -1; // -1 means that we just returned null
  function stdinWrapper() {
    try {
      if (inputIndex === -1) {
        let text = stdin();
        if (text === undefined || text === null) {
          return null;
        }
        if (typeof text !== "string") {
          throw new TypeError(
            `Expected stdin to return string, null, or undefined, got type ${typeof text}.`
          );
        }
        if (!text.endsWith("\n")) {
          text += "\n";
        }
        input = encoder.encode(text);
        inputIndex = 0;
      }

      if (inputIndex < input.length) {
        let character = input[inputIndex];
        inputIndex++;
        return character;
      } else {
        inputIndex = -1;
        return null;
      }
    } catch (e) {
      // emscripten will catch this and set an IOError which is unhelpful for
      // debugging.
      console.error("Error thrown in stdin:");
      console.error(e);
      throw e;
    }
  }
  return stdinWrapper;
}

/**
 * Make the home directory inside the virtual file system,
 * then change the working directory to it.
 *
 * @param path
 * @private
 */
export function setHomeDirectory(path: string) {
  Module.preRun.push(function () {
    const fallbackPath = "/";
    try {
      Module.FS.mkdirTree(path);
    } catch (e) {
      console.error(`Error occurred while making a home directory '${path}':`);
      console.error(e);
      console.error(`Using '${fallbackPath}' for a home directory instead`);
      path = fallbackPath;
    }
    Module.ENV.HOME = path;
    Module.FS.chdir(path);
  });
}
